Skip to content

原型继承

原型是JavaScript的一个核心知识点。参考:原型与原型链

原型

所有的对象都是由其构造函数创建,基础对象最基本的构造函数是Obecjt()。

  • 每个对象都有一个__proto__的属性,指向该对象构造函数的原型,该属性也被称为隐式原型。
  • 每个函数都有一个"prototype"的属性,表示这个构造函数的原型,函数的原型实际上是一个对象,并且该对象有一个construct的属性,指向构造函数本身。

当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会委托它的__proto__(即它的构造函数的prototype)中寻找,如果一直找到最上层都没有找到,那么就宣告失败,返回undefined。最上层是 Object.prototype.__proto__ === null

js
function A() {}
function B(a) {
    this.a = a;
}
function C(a) {
    if (a) {
        this.a = a;
    }
}

A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;

console.log(new A().a); // 1
console.log(new B().a); // undefined 只有当对象没有从它自己身上找到对应的属性才会委托原型查找
console.log(new C(2).a); // 2
js
var F = function () {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}

var f = new F()
console.log(f.a) // function f的构造函数是F,其原型链只有有Object.prototype,没有Function.prototype
console.log(f.b) // undefined

console.log(F.a) // function, F的构造函数是Function,其原型链上有Function.prototype和Object.prototype
console.log(F.b) // function

new的过程与实现

new关键字调用函数(new ClassA(…))的具体步骤

  • 创建一个新对象,如var obj = {}
  • 设置新对象的constructor属性为构造函数的名称,设置新对象的__proto__属性指向构造函数的prototype对象
  • 使用新对象调用函数,函数中的this被指向新实例对象
    • 如果构造函数返回的是this或基础类型,则返回新对象
    • 如果构造函数返回的是引用对象,则实际返回的是这个引用对象,之前的操作将被抛弃
  • 将初始化完毕的新对象地址,保存到等号左边的变量中

实现new方法

js
function New(fn) {
    return function (...args) {
        var obj = {}
        obj.__proto__ = fn.prototype
        var res = fn.call(obj, ...args)
        // todo 判断res的类型,如果为引用类型则需要返回res
        return obj
    }
}

JavaScript实现继承的N种方式

在JS中可以通过下面几种方式实现继承

原型链继承

js
function Parent(name) { 
    this.name = name;
}
Parent.prototype.sayName = function() {
    console.log('parent name:', this.name);
}
function Child(name) {
    this.name = name;
}
Child.prototype = new Parent('father');
Child.prototype.constructor = Child;
Child.prototype.sayName = function() {
    console.log('child name:', this.name);
}
var child = new Child('son');
child.sayName();    // child name: son
// 这种方法存在两个缺陷:
// 1.子类型无法给超类型传递参数,在面向对象的继承中,我们总希望通过 var child = new Child('son', 'father'); 让子类去调用父类的构造器来完成继承。而不是通过像这样 new Parent('father') 去调用父类。
// 2.Child.prototype.sayName 必须写在 Child.prototype = new Parent('father') 之后,不然就会被覆盖掉。

类继承

js
function Parent(name) { 
    this.name = name;
}
Parent.prototype.sayName = function() {
    console.log('parent name:', this.name);
}
Parent.prototype.doSomthing = function() {
    console.log('parent do something!');
}
function Child(name, parentName) {
    Parent.call(this, parentName);
    this.name = name;
}
Child.prototype.sayName = function() {
    console.log('child name:', this.name);
}
var child = new Child('son');
child.sayName();      // child name: son
child.doSomthing();   // TypeError: child.doSomthing is not a function
// 解决了原型链继承带来的问题,可以向父类传递构造参数
// 但存在缺陷:每次创建一个 Child 实例对象时候都需要执行一遍 Parent 函数,无法复用一些公用函数。

组合式继承

js
function Parent(name) { 
    this.name = name;
}
Parent.prototype.sayName = function() {
    console.log('parent name:', this.name);
}
Parent.prototype.doSomething = function() {
    console.log('parent do something!');
}
function Child(name, parentName) {
    Parent.call(this, parentName);      // 第二次调用
    this.name = name;
}

Child.prototype.sayName = function() {
    console.log('child name:', this.name);
}
Child.prototype = new Parent();         // 第一次调用
Child.prototype.construtor = Child;
var child = new Child('son');
child.sayName();      
child.doSomething(); 
// 第一次调用构造函数显然是没有必要的,因为第一次调用构造函数时候不需要函数内部的那些实例属性,这么写只是想获得其原型上的方法罢了
// 下面的寄生组合式继承解决了这个问题

寄生组合式继承

js
function Parent(name) {
    this.name = name;
}
Parent.prototype.sayName = function() {
    console.log('parent name:', this.name);
}
function Child(name, parentName) {
    Parent.call(this, parentName);  
    this.name = name;    
}
Child.prototype = Object.create(Parent.prototype);   //修改
Child.prototype.construtor = Child;
Child.prototype.sayName = function() {
    console.log('child name:', this.name);
}
var parent = new Parent('father');
parent.sayName();      // parent name: father
var child = new Child('son', 'father');
child.sayName();       // child name: son

ES6 Class

ES6中新增了class关键字,用于取代之前构造函数初始化对象的形式,从语法上更加符合面向对象的写法。需要注意的是

  • 使用extends即可实现继承,更加符合经典面向对象语言的写法,如 Java
  • 子类的constructor一定要执行super(),以调用父类的constructor
js
class A {
    say(){
        console.log("foo");
    }
}
let a = new A();
// ES6中的class是语法糖,构造函数class的prototype是无法修改其他指向的,因此下面赋值会默认失败
A.prototype = {
    say(){
        console.log("bar");
    }
}

a.say(); // "foo"
let b = new A();
b.say(); // "foo"

// 但还是可以向原型上添加其他方法
 A.prototype.say2 = function(){
    console.log("say2");
}
b.say2() // say2

instanceof计算规则

参考instanceof MDN

instanceof 运算符用于检测右侧构造函数的prototype属性是否出现在左侧实例对象的原型链上。

下面是一个比较简易的实现

js
function instance_of(L, R) {
    while(true){
        if(L === null) return false
        L = L.__proto__
        if(L === R.prototype) return true
    }
}

看下面这个例子

js
var tmp = {};
var A = function () { };
A.prototype = tmp;

var a = new A();
A.prototype = {};

var b = Object.create(tmp); // b.__proto__ = tmp
b.constructor = A.constructor;

var a2 = new A()
console.log(a instanceof A); // false,此时a.__proto__ 为 tmp
console.log(b instanceof A); // false,此时b.__proto__ 为 tmp
console.log(a2 instanceof A); // true,此时a2.__proto__ 为 {}