前端基础详解面向对象构造函数原型与原型链继承

38次阅读

共计 32259 个字符,预计需要花费 81 分钟才能阅读完成。

大纲:

  • 一、理解对象

    • 1.1 属性类型
    • 1.2 属性方法
  • 二、创建对象

    • 2.1 简单方式创建
    • 2.2 工厂模式
    • 2.3 构造函数
    • 2.4 原型
  • 三、继承

    • 3.1 原型链
    • 3.2 借用构造函数
    • 3.3 组合继承(原型链 + 借用构造函数)
    • 3.4 原型式继承
    • 3.5 寄生式继承
    • 3.6 寄生组合继承
    • 3.6 总结
  • 四、ES6 继承

    • Class 关键字
    • extends 继承
    • super 关键字
    • 原生构造函数拓展
    • Mixin 模式的实现

一、理解对象

ECMAScript 中没有类的概念,因此它的对象也与基于类的语言的对象有所不同。
ECMA-262 把对象定义为“无序属性的集合,其属性可以包含基本值,对象或者函数”。对象的每个属性或方法都有一个名字,而每个名字映射到一个值。我们可以把 ECMAScript 的对象想象成散列表:无非就是一组键值对,其值可以是数据或函数。
每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员定义的类型。

1.1 属性类型

ECMAScript 中有两种属性:数据属性和访问器属性。

1.1.1 数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特征。

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性。能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,它们的这个特性默认值为 true。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。直接在对象上定义的属性,它们的这个特性默认值为 true。
  • [[Writable]]:表示能否修改属性的值。直接在对象上定义的属性,它们的这个特性默认为 true。
  • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined。

要修改属性默认的特性,必须通过 ES5 的 Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable 和 value。设置其中的一或多个值,可以更改对应的特征值。

var person = {};
Object.defineProperty(person, "name", {
    writable: false,      // 不能修改属性的值....
    configurable: false,  // 不能通过 delete 删除属性.....
    value: "Jason"        // 写入属性值
});
console.log(person.name); //Jason
person.name = "Cor";
console.log(person.name); //Jason
delete person.name;
console.log(person.name); //Jason

注意,一旦把属性设置为不可配置的,就不能再把它更改为可配置的了。此时再调用

Object.defineProperty()方法修改除 writable 之外的特性就会导致错误。var person = {};
Object.defineProperty(person, "name", {
    configurable: false,
    value: "Jason"
});
// 抛出错误
Object.defineProperty(person, "name", {
    comfogirable: true,    // 这行代码修改了特性导致报错
    value: "Cor"
});

在调用 Object.defineProperty()方法时,如果不指定 configurable、enumerable 和 writable 特性的默认值都是 false。

1.1.2 访问器属性

访问器属性不包含数据值,它包含一对 getter 和 setter 函数(不过,这两个函数都不是必须的)。在读取访问器属性时回调去 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性:

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性默认值为 true。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为 true。
  • [[Get]]:在读取属性时调用的函数。默认值为 undefined。
  • [[Set]]:在写入属性时调用的函数。默认值为 undefined。

访问器属性不能直接定义,必须使用 Object.defineProperty()方法来定义。
注意,一旦定义了取值函数 get(或存值函数 set),就不能将 writable 属性设为 true,或者同时定义 value 属性,否则会报错。

    var book = {
        _year: 2004,
        edition: 1
    };
    
    Object.defineProperty(book, "year", {get: function() {return this._year;},
        set: function(newValue) {    // 接受新值的参数
            if(newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    });
    book.year = 2005;            // 写入访问器,会调用 setter 并传入新值
    console.log(book.edition);  //2
var obj = {};

Object.defineProperty(obj, 'p', {
  value: 123,
  get: function() { return 456;}
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value

Object.defineProperty(obj, 'p', {
  writable: true,
  get: function() { return 456;}
});
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute

1.2 属性方法

  • Object.getOwnPropertyDescriptor()

该方法可以获取属性描述对象。它的第一个参数是一个对象,第二个参数是一个字符串,对应该对象的某个属性名。注意,该方法只能用于对象自身的属性,不能用于继承的属性。

var obj = {p: 'a'};

Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }
  • Object.getOwnPropertyNames()

该方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。下面例子中,obj.p1 是可遍历的,obj.p2 是不可遍历的。但是 Object.getOwnPropertyNames 会将它们都返回。

var obj = Object.defineProperties({}, {p1: { value: 1, enumerable: true},
  p2: {value: 2, enumerable: false}
});

Object.getOwnPropertyNames(obj)
// ["p1", "p2"]

与 Object.keys 的行为不同,Object.keys 只返回对象自身的可遍历属性的全部属性名。下面代码中,数组自身的 length 属性是不可遍历的,Object.keys 不会返回该属性。第二个例子的 Object.prototype 也是一个对象,所以实例对对象都会继承它,它自身的属性都是不可遍历的。

Object.keys([]) // []
Object.getOwnPropertyNames([]) // ['length']

Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
//  'valueOf',
//  'constructor',
//  'toLocaleString',
//  'isPrototypeOf',
//  'propertyIsEnumerable',
//  'toString']
  • Object.defineProperty(),Object.defineProperties()

Object.defineProperty()方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象。实例上面已经介绍。
如果一次性定义或修改多个属性,可以使用 Object.defineProperties 方法。

var obj = Object.defineProperties({}, {p1: { value: 123, enumerable: true},
  p2: {value: 'abc', enumerable: true},
  p3: {get: function () {return this.p1 + this.p2},
    enumerable:true,
    configurable:true
  }
});

obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"
  • Object.prototype.propertyIsEnumerable()

实例对象的 propertyIsEnumerable 方法返回一个布尔值,用来判断某个属性是否可遍历。

var obj = {};
obj.p = 123;

obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false

二、创建对象

2.1 简单方式创建

我们可以通过 new 的方式创建一个对象,也可以通过字面量的形式创建一个简单的对象。

var obj = new Object();
或
var obj = {};
// 为对象添加方法,属性
var person = {};
person.name = "TOM";
person.getName = function() {return this.name;}

// 也可以这样
var person = {
    name: "TOM",
    getName: function() {return this.name;}
}

这种方式创建对象简单,但也存在一些问题:创建出来的对象无法实现对象的重复利用,并且没有一种固定的约束,操作起来可能会出现这样或者那样意想不到的问题。如下面这种情况。

var a = new Object;
var b = new Object;
var c = new Object;
c[a]=a;
c[b]=b;
console.log(c[a]===a); // 输出什么 false
该题的详细解析请参考文章一条面试题

2.2 工厂模式

当我们需要创建一系列相似对象时,显然上面简单的对象创建方式已经不可以了,这会使代码中出现很对重复的编码,造成代码冗余难维护。就以 person 对象为例,假如我们在实际开发中,需要一个名字叫做 TOM 的 person 对象,同时还需要另外一个名为 Jake 的 person 对象,虽然它们有很多相似之处,但是我们不得不重复写两次。没增加一个新的 person 对象,就重复一遍代码,听起来就是很崩溃的。

var perTom = {
    name: 'TOM',
    age: 20,
    getName: function() {return this.name}
};

var perJake = {
    name: 'Jake',
    age: 22,
    getName: function() {return this.name}
}

我们可以使用工厂模式的方式解决这个问题。顾名思义,工厂模式就是我们提供一个模子,然后通过这个模子复制出我们需要的对象。需要多少,就复制多少。

var createPerson = function(name, age) {

    // 声明一个中间对象,该对象就是工厂模式的模子
    var o = new Object();

    // 依次添加我们需要的属性与方法
    o.name = name;
    o.age = age;
    o.getName = function() {return this.name;}

    return o;
}

// 创建两个实例
var perTom = createPerson('TOM', 20);
var PerJake = createPerson('Jake', 22);

工厂模式帮助我们解决了重复代码的问题,可以快速的创建对象。但是这种方式仍然存在两个问题:没有办法识别对象实例的类型

var obj = {};
var foo = function() {}

console.log(obj instanceof Object);  // true
console.log(foo instanceof Function); // true
console.log(perTom instancceof (类名??));  // 发现好像并不存在一个 Person 类

因此,在工厂模式的基础上,我们需要使用构造函数的方式来解决这个问题。

2.3 构造函数

2.3.1 new 关键字

在 Javascript 中,new 关键字十分神奇,可以让一个函数变的与众不同。看下面这个例子。

function demo() {console.log(this);
}

demo();  // window,严格模式下 this 指向 undefined
new demo();  // demo

从这个例子我们可以看到,使用 new 之后,函数内部发生了一些变化,this 指向发生了改变。那么 new 关键字到底都做了什么事情呢?

// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别
var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {return this.name;}
}

// 将构造函数以参数形式传入
function New(func) {

    // 声明一个中间对象,该对象为最终返回的实例
    var res = {};
    if (func.prototype !== null) {

        // 将实例的原型指向构造函数的原型
        res.__proto__ = func.prototype;
    }

    // ret 为构造函数执行的结果,这里通过 apply,将构造函数内部的 this 指向修改为指向实例对象 res
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));

    // 当我们在构造函数中明确指定了返回对象时,那么 new 的执行结果就是该返回对象(即在构造函数中明确写了 return this;)
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {return ret;}

    // 如果没有明确指定返回对象,则默认返回 res,这个 res 就是实例对象
    return res;
}

// 通过 new 声明创建实例,这里的 p1,实际接收的正是 new 中返回的 res
var person1 = New(Person, 'tom', 20);
console.log(person1.getName());

// 当然,这里也可以判断出实例的类型了
console.log(p1 instanceof Person); // true

JavaScript 内部会通过一些特殊处理,将 var p1 = New(Person,’tom’, 20); 等效于 var person1 = new Person(’tom’, 20); 我们熟悉的这种形式。具体是怎么处理的,暂时没法作出解释,需要更深入的了解原理。

2.3.2 构造函数创建对象

为了能够判断实例与对象的关系,我们就使用构造函数来搞定。
像 Object 和 Array 这样的原生构造函数,在运行时自动出现在执行环境中。我们也可以创建自定义的构造函数,从而定义对象类型的属性和方法。例如,

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.getName = function() {console.log(this.name);
    }
}
var person1 = new Person("Jason", 18, "WEB”);
var person2 = new Person("Cor", 19, "WEB");
console.log(person1.getName());   //Jason
console.log(person1 instanceof Person);  //true

构造函数模式和工厂模式存在一下不同之处

  • 没有显示的创建对象(new Object() 或者 var a = {})
  • 直接将属性和方法赋给 this 对象
  • 没有 return 语句

关于构造函数,如果你暂时不能够理解 new 的具体实现,就先记住下面这几个结论:

  • 与普通函数相比,构造函数并没有任何特别的地方,首字母大写只是我们开发中的约定规定,用于区分普通函数
  • new 关键字让构造函数拥有了与普通函数不同的许多特点,new 的过程中,执行了下面的过程:

    • 声明一个中间对象,即实例对象
    • 将该中间对象的原型指向构造函数原型(res.__proto__ = func.prototype)
    • 将构造函数 this,指向该中间对象
    • 返回该中间对象,及返回实例对象

2.3.3 把构造函数当普通函数

// 当作构造函数使用
var person = new Person("Jason", 18, "web");
person.getName();        //“Jason"

// 作为普通函数调用
Person("Cor", 19, "web");    // 添加到 window
window.getName();        //“cor"

// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 22, "web");
o.getName();               //"kriten"

当在全局作用域中调用一个函数时,this 对象总是指向 Global 对象 (在浏览器中的 window 对象),最后使用了 call() ( 或者 apply() ) 在某个特殊对象的作用域中调用 Person()函数。这里是在对象 o 的作用域调用的,因此调用后 o 就拥有了所有属性和方法。

2.3.4 构造函数的问题

构造函数的主要问题:上述例子中,每一个 getName 方法实现的功能其实是一模一样的,但是由于分别属于不同的实例,就不得不一直不停的为 getName 分配空间。

person1.getName == person2.getName;  //false

我们对构造函数稍加修改,在构造函数内部我们把 getName 属性设置成等于全局的 getName 函数。由于构造函数的 getName 属性包含的是一个指向函数的指针,因此 person1 和 person2 对象就共享了在全局作用域中定义的同一个 getName()函数。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.getName = getName;
}

function getName() {console.log(this.name);
}

var person1 = new Person("Jason", 18, "WEB");
var person2 = new Person("Cor", 19, "WEB”);
person1.getName == person2.getName;  //true

2.4 原型

我们创建的每一个函数,都可以有一个 prototype 属性,该属性指向一个对象,这个对象就是我们说的原型对象。原型对象的用途是:包含所有可以由构造函数实例共享的属性和方法。按照字面理解就是,prototype 就是由构造函数创建的实例对象的原型对象,使用原型对象的好处就是可以让所有实例共享原型对象所包含的方法,属性。

2.4.1 理解原型对象

上面说了,每一个函数创建的时候,都会依据某些规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型的对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。以上面的例子来说,也就是 Person.prototype.constructor 指向 Person。
创建了自定义构造函数之后,其原型对象默认只会取得 constructor 属性;至于其它方法,则都是从 Object 继承而来的。当调用构造函数 new 一个新实例后(person1),实例都有一个__proto__属性,该属性指向构造函数的原型对象(Person.prototype),通过这个属性,让实例对象也能够访问原型对象上的方法。因此,当多有的实例都能够通过__proto__访问到原型对象时,原型对象的方法与属性就变成了共有方法与属性。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.getName = function() {console.log(this.name);
}

var person1 = new Person("Jason", 20);
var person2 = new Person("Tim", 40);

console.log(person1.getName == person2.getName);     //true



ECMA-262 第五版中管这个指针叫 [[Prototype]],虽然在脚本中没有标准的方式访问[[Prototype]],单 Firefox,Safari 和 Chrome 在每个对象上都支持一个属性__proto__;而在其他实现中,这个属性对脚本则是完全不可见的。不过需要明确的真正重要一点就是, 这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
虽然所有的实现中都无法访问到 [[Prototype]],但是可以通过 isPrototypeOf() 方法来确定对象之间是否存在这种关系。从本质上讲,如果 [[Prototype]] 指向调用 isPrototypeOf()方法的对象(Person.prototype),那么这个方法就会返回 true。

console.log(Person.prototype.isPrototypeOf(person1))  //true
console.log(Person.prototype.isPrototypeOf(person2))  //true

ES5 增加了一个新方法,叫 Object.getPrototypeOf(),在所有支持的实现中,这个方法返回 [[Prototype]] 的值,可以方便的获取一个对象的原型。

console.log(Object.getPrototypeOf(person1) == Person.prototype); //true
console.log(Object.getPrototypeOf(person1).getName());     //"Jason"
    
搜索机制:

当我们访问对象的属性或者方法时,会优先访问实例对象自身的属性和方法。当代码执行到读取对象的属性 a 时,都会执行一次搜索。搜索首先从对象的实例本身开始。如果在实例中找到属性 a,则返回该属性的值;如果没找到,则继续搜索之震惊指向的原型对象,在原型对象中查找属性 a,如果在原型中找到这个属性,则返回该属性。简单的说,就是会一层层搜索,若搜索到则返回,没搜索到则继续下层搜索。
虽然可以通过实例访问原型的值,但是却不能通过对象实例重写原型的值。如果我们为水添加了一个属性,并且该属性名和实例原型中的一个属性同名,就会在实例中创建该属性,该属性户屏蔽原型中的相同属性。因为搜索的时候,首先在实例本身搜索,查找到后直接返回实例中属性的值,不会搜索到原型中。

function Person() {}

Person.prototype.name = "Jason";
Person.prototype.age = 29;
Person.prototype.job = "Web";
Person.prototype.getName = function() {console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();
person1.name = "Cor";
person1.getName();         //"Cor"
person2.getName();        //"Jason"

若想能够访问原型中的属性,只要用 delete 操作符删掉实例中的属性即可。

delete person1.name;
person1.getName();       //"Jason"
    

hasOwnProperty()

该方法可以检测一个属性是存在在实例中,还是存在于原型中。这个方法继承于 Object,只有在给定属性存在于对象实例中时,才会返回 true

console.log(person1.hasOwnProperty("name"));        //false
person1.name = "Cor";
console.log(person1.hasOwnProperty("name"));        //true;

2.4.2 in 操作符

in 操作符有两种使用方式:单独使用和在 for-in 循环中使用。
在单独使用时,in 操作符在通过对象能访问到给定属性时就会返回 true,无论属性存在于实例还是原型中。

console.log("name" in person1); true

in 的这种特殊性做常用的场景之一,就是判断当前页面是否在移动端打开。

   isMobile = 'ontouchstart' in document;

// 很多人喜欢用浏览器 UA 的方式来判断,但并不是很好的方式

2.4.3 更简单的原型语法

可以用一个包含所有属性和方法的对象字面量来重写整个原型对象。

function Person(){}

Person.prototype = {
    name : "Jason",
    age : 29,
    job : "Web",
    getName : function() {console.log(this.name)
    }
};

用对象字面的方法和原来的方法会有区别:constructor 属性不再指向 Person 了。因为这种写法,本质上是修改了 Person.prototype 对象的引用,将引用从原来的默认值修改为了这个新对象 {} 的引用,constructor 属性也变成了新对象 {} 的 constructor 属性(指向 Object 构造函数),不再指向 Person。尽管 instanceof 操作符能返回正确的结果,但是 constructor 已经无法确定对象的类型了。

var friend = new Person();

console.log(friend instanceof Object);  //true
console.log(friend instanceof Person);    //true
console.log(friend.constructor == Person);  //false
console.log(friend.constructor == Object);  //true

如果 construct 的值很重要,我们可以像下面这样特意将它设置回适当的值。

function Person(){}

Person.prototype = {
    constructor: Person,
    name : "Jason",
    age : 29,
    job : "Web",
    getName : function() {console.log(this.name)
    }
};

2.4.4 原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们在原型对象上所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也一样。下面这个例子中,friend 实例是在添加 sayHi 方法之前创建的,但它仍然可以访问新方法。这是因为实例与原型之间只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi 属性并返回值。

var friend = new Person();

Person.prototype.sayHi = function() {alert("hi");
};

friend.sayHi();        //"hi"

但是,如果是通过 {} 这种重写原型对象的情况,就和上边不一样了。因为 new 实例时,实例中的__proto__属性指向的是最初原型,而把原型修改为新的对象 {} 就等于切断了构造函数与最初原型之间的联系,同时实例中仍然保存的是最初原型的指针,因此无法访问到构造函数的新原型中的属性。请记住:实例只与原型有关,与构造函数无关。

function Person(){}

var friend = new Person();

Person.prototype = {
    constructor: Person,
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {console.log(this.name)
    }
};

friend.sayName();   //error,friend.sayName is not a function

如图,重写原型对象切断了现有原型与任务之前已经存在的对象实例之间的联系;friend 实例引用的仍然是最初的原型,因此访问不到 sayName 属性。
注意,若想使用对象字面量重写原型,要在创建实例之前完成。

2.4.5 原生对象的原型

原型创建对象的重要性不仅体现在创建自定义对象方面,就连所有原生的引用类型都采用这种模式创建。所有原生引用类型 (Object、Array、String,等等) 都在其构造函数的原型上定义了方法。

console.log(Array.prototype.sort);            //function(){…}        
console.log(String.prototype.substring);      //function(){...}

通过原生对象的原型,我们也可以自定义新的方法。

String.prototype.startsWith = function(text) {return this.indexOf(text) == 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true
    

巩固一下原型相关的知识点,我们以 window 这个对象为例,来查看一下各个属性值

三、继承

3.1 原型链

原型对象其实也是普通的对象。几乎所有的对象都可能是原型对象,也可能是实例对象,而且还可以同时是原型对象与实例对象。这样的一个对象,正是构成原型链的一个节点。
我们知道所有的函数都有一个叫做 toString 的方法,那么这个方法到底是从哪来的呢?先声明一个函数:function add() {};,通过下图来看一下这个函数的原型链情况。

其中 add 是 Function 对象的实例。而 Function 的原型对象同时又是 Object 原型的实例。这样就构成了一条原型链。原型链的访问,其实跟作用域链有很大的相似之处,他们都是一次单向的查找过程。因此实例对象能够通过原型链,访问到处于原型链上对象的所有属性与方法。这也是 foo 最终能够访问到处于 Object 原型对象上的 toString 方法的原因。
我们再来看一个例子:

function SuperType(){this.property = true;}

SuperType.prototype.getSuperValue = function(){return this.property;}

function SubType(){this.subproperty = false;}

// 继承了 SuperType
//SubType 的原型对象等于 SubperType 的实例,// 这样 SubType 内部就会有一个指向 SuperType 的指针从而实现继承
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){return this.subproperty;}

var instance = new SubType();
console.log(instance.getSuperValue());  //true

SubType 继承了 superType,而继承是通过创建 SuperType 实例,并将该实例赋给 SubType.prototype 实现的。原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。这个例子中的实例、构造函数和原型之间的关系如图:

注意,instance.constructor 现在指向的是 SuperType,是因为 subtype 的原型指向了另一个对象 SuperType 的原型,而这个原型对象的 constructor 属性指向的是 SuperType。

    通过实现原型链,本质上扩展了前面说的原型搜索机制。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。那上面这个例子来说,调用 instance.getSuperValue()会经历三个搜索步骤:
  1. 搜索 instance 实例;未搜到;
  2. 搜索 SubType.prototype,未搜索到;
  3. 搜索 SuperType.prototype,找到该属性,返回值。

3.1.1 默认原型

所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内布指针,指向 Object.prototype。这也正是所有自定义类型都会继承 toString()、valueOf()等默认方法的根本原因。所以上面的例子展示的原型链中还应该包含另一个继承层次。

3.1.2 确定原型和实例的关系

有两种方式可以确认:instanceof 操作符以及 isPrototypeOf()方法。

console.log(instance instanceof Object);    //true    
console.log(instance instanceof SuperType);    //true
console.log(instance instanceof SubType);    //true

console.log(Object.prototype.isPrototypeOf(instance));      //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance));   //true

由于原型链的关系,可以说 instance 是 Object,SuperType,SubType 中任何一个类型的实例,因此都返回 true。同样的,只要是原型链中出现过的原型,都可以说是该原型链所派生实例的原型。

3.1.3 谨慎定义方法

子类型 (SubType) 有时候需要重写超类型 (SuperType) 中的某个方法,或者需要添加超类型中没有的方法,但不管怎样,给原型添加方法一定要在替换原型语句之后。避免出现 2.4.4 中出现的问题。
另外要注意,在通过原型链实现继承时,不能使用对象字面量创建原型方法,这样做会重写原型。

function SuperType(){this.property = true;}

SuperType.prototype.getSuperValue = function(){return this.property;}

function SubType(){this.subproperty = false;}

// 继承超类型
SubType.prototype = new SuperType();

// 添加新方法
SubType.prototype.getSubValue = function(){return this.subproperty;}

// 重写超类型中的方法
SubType.prototype.getSuperValue = function(){return false;}

// 使用字面量添加新方法,会导致上面代码无效
SubType.prototype = {getSubValue : function(){return this.subproperty;},
    someOtherMethod : function(){return false;}
}

3.1.4 原型链的问题

function SuperType(){this.colors = ["red", "blue", "green"];
}

function SubType(){}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);  //"red, blue, green, black"

var instance2 = new SubType();
console.log(instance2.colors);    //"red, blue, green, black"

这个例子中的 SuperType 构造函数定义了一个 colors 属性,该属性包含一个数组 (引用类型值)。SuperType 的每个实例都会有各自包含自己数组的 colors 属性。当 SubType 通过原型链继承了 SuperType 之后,SubType.prototype 就变成了 SuperType(), 所以它也用用了一个 colors 属性。就跟专门创建了一个 SubType.prototype.colors 属性一样。结果 SubType 得所有实例都会共享这一个 colors 属性。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。所以在实际运用中很少会单独使用原型链。

3.2 借用构造函数

在子类型构造函数中调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过使用 apply()和 call()方法也可以在新创建的对象上执行构造函数。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

function SubType(){
    // 继承了 SuperType,同时还传递了参数
    SuperType.call(this, "Jason”);

    // 执行上边这个语句,相当于把 SuperType 构造函数的属性在这里复制了一份
    //this.name = "Jason”;
    //this.colors = ["red", "blue", "green"];

    // 实例属性
    this.age = 18;
}

var instance1 = new SubType();
instance1.colors. push("black");
console.log(instance1.colors);         //"red,blue,green,black"
console.log(instance1.name);        //"Jason"
console.log(instance1.age);            //18

var instance2 = new SubType();
console.log(instance2.colors);        //"red,blue,green"

为了确保 SuperType 构造函数不会重写子类型属性,可以在调用超类型构造函数之后,再添加需要在子类型的定义的私有属性。
这个方式实现继承仍然存在问题:

  • 方法都在构造函数中定义,每个实例创建后都会为构造函数的属性分配自己的内存,复用方法就无从谈起。
  • 而且,即使在超类型的原型中定义了公有属性,但这些属性对于子类型而言是不可见的,所以采用这种方式继承,就要把所有属性写在构造函数中

所以这种方式在实际开发中是很少单独这样使用的。

3.3 组合继承(原型链 + 借用构造函数)

组合继承(combination inheritance),有时候也叫做经典继承,指的是将原型链和借用构造函数的继承方式组合到一块,从而发挥二者的长处的一种继承方式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既可以通过在原型上定义方法实现了函数复用,又可以在构造函数中定义方法保证每个实例都有自己的私有属性。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){console.log(this.name);
};

function SubType(name, age){
    // 继承属性
    SuperType.call(this, name);
    
    this.age = age;
}

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){console.log(this.age);
};

var instance1 = new SubType("Jason", 18);
instance1.colors.push("black");
console.log(instance1.colors);    //"red,blue,green,black"
instance1.sayName();            //"Jason"
instance1.sayAge();                //18

var instance2 = new SubType("Cor", 20);
console.log(instance2.colors)    //"red,blue,green"
instance2.sayName();            //"Cor"
instance2.sayAge();                //20
    

组合继承的问题

组合继承虽然是现在 javascript 最常用的继承模式,但是它也有不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型的构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "bule", "grenn"];
}

SuperType.prototype.sayName = function(){console.log(this.name);
}

function SubType(name, age){SuperType.call(this, name);          // 第二次调用 SuperType
 
    this.age = age;
}

SubType.prototype = new SuperType();     // 第一次调用 SuperType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){console.log(this.age);
}

有注释的两行代码是调用 SuperType 构造函数的代码,第一次调用 SuperType 构造函数时,SubType.prototype 会有 SuperType 的实例属性。第二次调用 SuperType 的构造函数时 SubType 会在构造函数中添加了 SuperType 的实例属性。当创建 SubType 的实例它的 [[Prototype]] 和自身上都有相同属性。根据搜索机制自身的属性就会屏蔽 SubType 原型对象上的属性。等于原型对象上的属性是多余的了。如图所示,有两组 name 和 colors 属性:一组在实例上,一组在 Subtype 原型中。这就是调用两次构造函数的结果。

3.4 原型式继承

基本思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

function object(o){function F(){}
    F.prototype = o;
    return new F();}

在 object()函数内部,先创建了一个临时性的构造函数,然后传入的对象作为这个构造函数的原型,最后返回了这个临时构造函数的一个新实例。从本质上讲,object()对传入其中的对象执行了一次复制。
在 ECMAScript5 通过新增 Object.create()方法规范了原型式继承。这个方法接收两个参数:第一个用于做新对象原型的对象;第二个参数可选,为新对象定义额外的属性的对象。在传入一个参数的情况下,

Object.create()和 object()方法的行为相同。var person = {
    name: "Jason",
    friends: ["Cor", "Court", "Sam"]
};

var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person, {
    name: {value: "Greg"}
});
yetAnotherPerson.friends.push("Barbie");

console.log(yetAnotherPerson.name);    //"Greg"
console.log(anotherPerson.name);       //"Jason"
console.log(person.friends);           //"Cor,Court,Sam,Rob,Barbie"
console.log(anotherPerson.__proto__);               //"Cor,Court,Sam,Rob,Barbie"

这种原型式继承,要求必须有一个对象可以作为另一个对象的基础,把这个它传递给 object()对象,然后再根据需求对得到的对象加以修改。在这个例子中,person 对象可以作为另一个对象的基础,把它传入 object()函数后返回一个新对象。这个新对象是将 person 作为原型,这意味着 person.friend 不仅属于 person,而且被 anotherPerson 和 yetAnotherPerson 共享。

3.5 寄生式继承

寄生式(parasitic)继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

function createAnother(original){var clone = object(original);     // 通过调用函数创建一个新对象
    clone.sayHi = function(){         // 以某种方式来增强这个对象
         alert("Hi");
    };
    return clone;                     // 返回这个对象
}

// 使用 createAnother
var person = {
    name: "Jason",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();     //"hi"

这个例子中的代码基于 person 对象返回了一个新对象——anotherPerson。新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi()方法。

3.6 寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承实例属性,通过寄生式继承方式来继承原型属性。其基本思路就是:不必为指定子类型的原型二调用超类型的构造函数,我们需要的只是超类型原型的一个副本而已。本质上就是,使用寄生式继承超类型的原型,然后将结果指定给子类型的原型。寄生组合式继承的基本模式如下:

function inheritPrototype(subType, superType){var prototype = object(superType.prototype);   // 创建对象
    prototype.constructor = subType;               // 增强对象
    subType.prototype = prototype                  // 指定对象
}

这个实例的 inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接受两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。最后一步,将新创建的对象 (即副本) 赋值给子类型的原型。

function object(o){function F(){}    // 创建个临时构造函数
    F.prototype = o;  //superType.prototype
    return new F();   // 返回实例}

function inheritPrototype(subType, superType){
    /*  创建对象
        传入超类型的原型,通过临时函数进行浅复制,F.prototype 的指针就指向 superType.prototype,在返回 new F()    
    */
    var prototype = object(superType.prototype);   
    prototype.constructor = subType;               // 增强对象
    /*  指定对象
        子类型的原型等于 F 类型的实例, 当调用构造函数创建一个新实例后,该实例会包含一个 [[prototype]] 的指针指向构造函数的原型对象,所以 subType.prototype 指向了超类型的原型对象这样实现了继承,因为构造函数 F 没有属性和方法这样就子类型的原型中就不会存在超类型构造函数的属性和方法了。*/
    subType.prototype = prototype                  //new F();}

function SuperType(name){
    this.name = name;
    this.colors = ["red", "bule", "grenn"];
}

SuperType.prototype.sayName = function(){console.log(this.name);
}

function SubType(name, age){SuperType.call(this, name);          
 
    this.age = age;
}

inheritPrototype(SubType, SuperType);
// 即等价于:SubType.prototype = Object.create(Super.Prototype);
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function(){console.log(this.age);
}

var ins1 = new SubType("Jason", 18);

3.7 总结

这里我们对六种继承方式的基本思想,具体实现,优缺点做一个简单的总结,巩固一下我们上面学到的知识。
继承方式:原型链继承
基本思想:利用原型链来实现继承,超类的一个实例作为子类的原型
具体实现:

// 子类
function Sub(){this.property =‘Sub Property’;}
Sub.prototype = new Super();
// 注意这里 new Super()生成的超类对象并没有 constructor 属性, 故需添加上
Sub.prototype.constructor = Sub;

优缺点:
优点:

  • 简单明了,容易实现
  • 实例是子类的实例,实际上也是父类的一个实例
  • 父类新增原型方法 / 原型属性,子类都能访问到

缺点:

  • 所有子类的实例的原型都共享同一个超类实例的属性和方法
  • 在创建子类型的实例时,不能向超类型的构造函数传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数

继承方式:借用构造函数
基本思想:通过使用 call、apply 方法可以在新创建的对象上执行构造函数, 用父类的构造函数来增加子类的实例
具体实现:

// 子类 
function Sub(){Super.call(this);
    this.property = 'Sub Property’;
}

优缺点:
优点:

  • 简单明了,直接继承超类构造函数的属性和方法
  • 可以传递参数

缺点:

  • 无法继承原型链上的属性和方法
  • 实例只是子类的实例,不是父类的实例

继承方式:组合继承
基本思想:利用构造继承和原型链组合。使用原型链实现对原型属性和方法的继承,用借用构造函数模式实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性
具体实现:

// 子类
function Sub(){Super.call(this);
  this.property = 'Sub Property’;
}
Sub.prototype = new Super();
// 注意这里 new Super()生成的超类对象并没有 constructor 属性, 故需添加上
Sub.prototype.constructor = Sub;

优缺点:
优点:

  • 解决了构造函数的两个问题
  • 既是父类实例,也是子类实例

缺点:

  • 调用两次超类型的构造函数,导致子类上拥有两份超类属性:一份在子类实例中,一份在子类原型上,且搜索时实例中属性屏蔽了原型中的同名属性

继承方式:原型式继承
基本思想:采用原型式继承并不需要定义一个类,传入参数 obj, 生成一个继承 obj 对象的对象
具体实现:

function object(obj){function F(){}; 
    F.prototype = obj; 
    return new F();}

优缺点:
优点:

  • 直接通过对象生成一个继承该对象的对象

缺点:

  • 不是类式继承,而是原型式继承,缺少了类的概念

继承方式:寄生式继承
基本思想:创建一个仅仅用于封装继承过程的函数,然后在内部以某种方式增强对象,最后返回对象
具体实现:

function object(obj){function F(){}
  F.prototype = obj;
  return new F();}
function createSubObj(superInstance){var clone = object(superInstance);
  clone.property = 'Sub Property’;
  return clone;
}

优缺点:
优点:

  • 原型式继承的一种拓展

缺点:

  • 依旧没有类的概念

继承方式:寄生组合式继承
基本思想:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,不必为了指定子类型的原型而调用超类型的构造函数,只需要超类型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型
具体实现:

function inheritPrototype(Super,Sub){var superProtoClone = Object.Create(Super.prototype);
  superProtoClone.constructor = Sub;
  Sub.prototype =  superProtoClone;
}
function Sub(){Super.call(this);
  Sub.property = 'Sub Property’;
}
inheritPrototype(Super,Sub);

优缺点:
优点:

  • 完美实现继承,解决了组合式继承带两份属性的问题

缺点:

  • 过于繁琐,故不如组合继承

四、ES6 继承

4.1 Class 关键字

ES6 中通过 class 关键字定义类。

class Parent {constructor(name,age){
        this.name = name;
        this.age = age;
    }
    speakSomething(){console.log("I can speek chinese");
    }
}

// 经 babel 转码之后,代码是:"use strict";

var _createClass = function () {function defineProperties(target, props) {for (var i = 0; i < props.length; i++) {var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }

    return function (Constructor, protoProps, staticProps) {if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
}();

function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function");
    }
}

var Parent = function () {function Parent(name, age) {_classCallCheck(this, Parent);

        this.name = name;
        this.age = age;
    }

    _createClass(Parent, [{
        key: "speakSomething",
        value: function speakSomething() {console.log("I can speek chinese");
        }
    }]);

    return Parent;
}();

可以看出类的底层还是通过构造函数去创建的。
注意一点,通过 ES6 创建的类,是不允许直接调用的。即在 ES5 中,可以直接运行构造函数 Parent()。但是在 ES6 中就不行,在转码的构造函数中有 _classCallCheck(this, Parent); 语句,防止通过构造函数直接运行。直接在 ES6 运行 Parent(),报错 Class constructor Parent cannot be invoked without‘new’,转码后报错Cannot call a class as a function。
转码中 _createClass 方法,它调用 Object.defineProperty 方法去给新创建的 Parent 添加各种属性。defineProperties(Constructor.prototype, protoProps)是给原型添加属性。如果你有静态属性,会直接添加到构造函数上defineProperties(Constructor, staticProps)。

4.2 extends 继承

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Parent {
    static height = 12
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    speakSomething(){console.log("I can speek chinese");
    }
}
Parent.prototype.color = 'yellow'


// 定义子类,继承父类
class Child extends Parent {
    static width = 18
    constructor(name,age){super(name,age);
    }
    coding(){console.log("I can code JS");
    }
}

var c = new Child("job",30);
c.coding()

转码之后的代码变成了这样

"use strict";

var _createClass = function () {function defineProperties(target, props) {for (var i = 0; i < props.length; i++) {var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }

    return function (Constructor, protoProps, staticProps) {if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
}();

function _possibleConstructorReturn(self, call) {if (!self) {throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

function _inherits(subClass, superClass) {if (typeof superClass !== "function" && superClass !== null) {throw new TypeError("Super expression must either be null or a function, not" + typeof superClass);
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function");
    }
}

var Parent = function () {function Parent(name, age) {_classCallCheck(this, Parent);

        this.name = name;
        this.age = age;
    }

    _createClass(Parent, [{
        key: "speakSomething",
        value: function speakSomething() {console.log("I can speek chinese");
        }
    }]);

    return Parent;
}();
Parent.height = 12;  // 注意,该方法并不在转码后的构造函数 function Parent 中,Parent.prototype.color = 'yellow';

// 定义子类,继承父类

var Child = function (_Parent) {_inherits(Child, _Parent);

    function Child(name, age) {_classCallCheck(this, Child);

        return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name, age));
    }

    _createClass(Child, [{
        key: "coding",
        value: function coding() {console.log("I can code JS");
        }
    }]);

    return Child;
}(Parent);

Child.width = 18;


var c = new Child("job", 30);
c.coding();

可以看到,构造类的方法没变,只是添加了_inherits 核心方法来实现继承,我们来重点分析一个这个方法做了什么。

  • 首先判断父类的实例
  • 然后执行
subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
        value: subClass,
        enumerable: false,
        writable: true,
        configurable: true
    }
});

// 这段代码翻译一下就是
function F(){}
F.prototype = superClass.prototype
subClass.prototype = new F()
subClass.prototype.constructor = subClass
  • 最后,subClass.__proto__ = superClass。

_inherits 方法的核心思想,总结一下就是下面这两句话:

subClass.prototype.__proto__ = superClass.prototype
subClass.__proto__ = superClass

那为什么这样一倒腾,它就实现了继承了呢?
首先 subClass.prototype.__proto__ = superClass.prototype保证了 c instanceof Parent 是 true,Child 的实例可以访问到父类的属性,包括内部属性,以及原型属性。其次,subClass.__proto__ = superClass,保证了 Child.height 也能访问到,也就是静态方法。

class Parent {}
class Child extends Parent {}

// for static propertites and methods
alert(Child.__proto__ === Parent); // true

// and the next step is Function.prototype
alert(Parent.__proto__ === Function.prototype); // true

// that's in addition to the"normal" prototype chain for object methods
alert(Child.prototype.__proto__ === Parent);

在内置对象中没有静态继承

请注意,内置类没有静态 [[Prototype]] 引用。例如,Object 具有 Object.defineProperty,Object.keys 等方法,但 Array,Date 不会继承它们。

Date 和 Object 之间毫无关联,他们独立存在,不过 Date.prototype 继承于 Object.prototype,仅此而已。
造成这个情况是因为 JavaScript 在设计初期没有考虑使用 class 语法和继承静态方法。

4.3 super 关键字

super 这个关键字,既可以当函数使用,也可以当对象使用。在这两种情况下,它的用法完全不同。

  • 第一种情况,super 作为函数调用时代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函数。

子类 B 的构造函数之中的 super()代表调用父类的构造函数,必须执行,否则会报错。

class A {}

class B extends A {constructor() {super();
  }
}

注意,super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B 的实例,因此 super()在这里相当于 A.prototype.constructor.call(this)。
new.target 指向当前正在执行的函数。可以看到,在 super()执行时,它指向的是子类 B 的构造函数,而不是父类 A 的构造函数。也就是说,super()内部的 this 指向的是 B。

class A {constructor() {console.log(new.target.name);
  }
}
class B extends A {constructor() {super();
  }
}
new A() // A
new B() // B

注意,作为函数时,super()只能用在子类的构造函数之中,用在其他地方会报错。

class A {}

class B extends A {m() {super(); // 报错
  }
}
  • 第二种情况,super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {constructor() {
    this.x = 2;
    this.y = 8;
  }
  p() {console.log(this.x);
  },
}
A.prototype.z = 10;

class B extends A {constructor() {super();
    this.x = 5;
    console.log(super.p());  //5;
  }
  getY() {return super.y;}
  getZ() {return super.z;}
}

let b = new B();
b.getY();  //undefined
b.getZ();  //10

在普通方法中,super 指向 A.prototype,所以 super.p()就相当于 A.prototype.p()。但是这里需要注意两点:

  • super 指向的是父类原型,所以定义在父类实例上的方法或属性,无法通过 super 调用。所以在 B 类的 getY()方法中调用 super.y 获取不到值。但是定义在父类原型上的方法就可以获取到,如 getZ()方法中。
  • ES6 规定,在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前子类的实例。在 B 类中调用 super.p(),this 指向 B 类实例,输出的结果为 5。super.p()实际上执行的是 super.p.call(this)。

由于 this 指向子类实例,所以如果通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性。

class A {constructor() {this.x = 1;}
}
class B extends A {constructor() {super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined,super 获取不到父类的实例属性
    console.log(this.x); // 3
  }
}

在静态方法中,super 作为对象指向父类,而不是父类的原型。另外,在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例。

class A {constructor() {this.x = 1;}
  static print() {console.log(this.x);
  }
}

class B extends A {constructor() {super();
    this.x = 2;
  }
  static m() {super.print();
  }
}

B.x = 3;
B.m() // 3

注意,

  • 使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
    class A {}
    
    class B extends A {constructor() {super();
            console.log(super); // 报错
        }
    }
  • 由于对象总是继承其他对象的,所以可以在任意一个对象中,使用 super 关键字。
    var obj = {toString() {return "MyObject:" + super.toString();
        }
     };
    
    obj.toString(); // MyObject: [object Object]

在内置对象中没有静态继承

内置类没有静态 __proto__引用。例如,Object 具有 Object.defineProperty,Object.keys 等方法,但 Array,Date 不会继承它们。
Date 和 Object 之间毫无关联,他们独立存在,不过 Date.prototype 继承于 Object.prototype,仅此而已。
造成这个情况是因为 JavaScript 在设计初期没有考虑使用 class 语法和继承静态方法。

4.4 原生构造函数拓展

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

以前,原生构造函数是无法继承的。比如,不能自己定义一个 Array 的子类。

function MyArray() {Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});

var colors = new MyArray();
colors[0] = "red";
colors.length  // 0
colors.length = 0;
colors[0]  // "red"

上面这个例子中定义了一个继承 Array 的 MyArray 类。但是,我们看到,这个类的行为与 Array 完全不一致。
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过 Array.apply()或者分配给原型对象都不行。原生构造函数会忽略 apply 方法传入的 this,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。
ES5 是先新建子类的实例对象 this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array 构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新 length 属性,这个内部属性无法在子类获取,导致子类的 length 属性行为不正常。
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承 Array 的例子。

class MyArray extends Array {constructor(...args) {super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined

extends 关键字不仅可以用来继承类,还可以用来继承原生的构造函数。Array,Map 等内置类也可以扩展。

class MyArray extends Array {constructor(...args) {super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined

注意,继承 Object 的子类,有一个行为差异。

class NewObj extends Object{constructor(){super(...arguments);
  }
}
var o = new NewObj({attr: true});
o.attr === true  // false

上面代码中,NewObj 继承了 Object,但是无法通过 super 方法向父类 Object 传参。这是因为 ES6 改变了 Object 构造函数的行为,一旦发现 Object 方法不是通过 new Object()这种形式调用,ES6 规定 Object 构造函数会忽略参数。

4.5 Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。

const a = {a: 'a'};
const b = {b: 'b'};
const c = {...a, ...b}; // {a: 'a', b: 'b’}

上面代码中,c 对象是 a 对象和 b 对象的合成,具有两者的接口。
下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

function mix(...mixins) {
  class Mix {constructor() {for (let mixin of mixins) {copyProperties(this, new mixin()); // 拷贝实例属性
      }
    }
  }

  for (let mixin of mixins) {copyProperties(Mix, mixin); // 拷贝静态属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }

  return Mix;
}

function copyProperties(target, source) {for (let key of Reflect.ownKeys(source)) {
    if ( key !== 'constructor'
      && key !== 'prototype'
      && key !== 'name'
    ) {let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

上面代码的 mix 函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。

class DistributedEdit extends mix(Loggable, Serializable) {// ...}

参考资料:
《详解面向对象、构造函数、原型与原型链》https://segmentfault.com/a/11…
《JavaScript 学习笔记 - 面向对象设计》https://segmentfault.com/a/11…
《js 对象创建方法汇总及对比》https://segmentfault.com/a/11…
《Class 的继承》http://es6.ruanyifeng.com/#do…
《ES6 Class 继承与 super》https://segmentfault.com/a/11…
《ES6 类以及继承的实现原理》https://segmentfault.com/a/11…

正文完
 0