原型和继承

原型

每个函数都有一个属性prototype,它指向函数的原型,默认情况下它是一个普通Object对象。

调用该构造函数所创建的实例的隐式原型指向该构造函数的原型对象。

JS同样存在由原型指向构造函数的属性constructor,即Func.prototype.constructor --> Func

JS中所有对象(除了null)都具有一个__proto__属性,该属性指向该对象的隐式原型。

Tkh7H1.png

JavaScript所有的对象本质上都是通过new 函数创建的,包括对象字面量的形式定义对象(相当于new Object()的语法糖)。

所有的函数本质上都是通过new Function创建的,包括ObjectArray等。所有的函数都是对象

实例对象在查找属性时,如果查找不到,就会沿着__proto__去与对象关联的原型上查找,如果还查找不到,就去找原型的原型,直至查到最顶层,这也就是原型链的概念。

TkhJ0I.png

  • 所有函数(包括Function)的__proto__指向Function.prototype

  • 自定义对象实例的__proto__指向构造函数的原型

  • 函数的prototype__proto__指向Object.prototype

  • Object.prototype.__proto__ --> null

继承

继承是一种代码复用的方式。在面向对象编程中,继承是一个很重要的点。

在JS中继承背后的原理是原型prototype, 这种实现继承的方式,我们称之为原型继承。

全局对象

JS中一些全局内置函数,分别为Functon, Array, Object.

1
2
3
console.log(Object); // -> ƒ Object() { [native code] }
console.log(Array); // -> ƒ Array() { [native code] }
console.log(Function); // -> ƒ Function() { [native code] }
  • 所有的数组对象,都是由全局内置函数Array创建的
  • 所有的object对象,都是由全局内置函数Object创建的
  • 所有的函数对象,都是由全局内置函数Function创建的

其他也是同理,比如:

1
2
3
1..__proto__ === Number.prototype; // true
1'.__proto__ === String.prototype; // true
true.__proto__ === Boolean.prototype; // true

__proto__

__proto__是一个内部属性,不建议对其进行直接操作。 而是建议通过prototype来进行操作。

一个对象的__proto__总是指向它的构造函数的prototype

构造函数指的是创建这个对象的函数, 比如 foo = new Foo(), 那么Foo就是foo的构造函数。

让我们来继续看一下上面的代码, 就不难理解了:

1
2
3
1..__proto__ === Number.prototype; // true
1'.__proto__ === String.prototype; // true
true.__proto__ === Boolean.prototype; // true

除此我们需要注意一点,那就是Object.prototype.__proto__ 值为 null。 其实也就是继承链的终点

原型链

为了能够明白原型链和继承,我们首先要知道“属性查找机制”。

当我们访问一个对象的属性的时候,引擎首先会在当前对象进行查找,如果找不到就会访问该对象的__proto__, 如果__proto__有了,就返回,如果没有则递归执行上述过程,直到__proto__null

继承的过程,直接依靠的是__proto__, 只不过就像我上面提到的__proto__ 只是一个指向构造函数原型的引用, 因此开发人员修改了构造函数的原型,就会影响到__proto__, 进而影响了对象的原型链。

当然你可以自己直接修改__proto__,但是不推荐!

1
2
3
4
var obj = {};
obj.__proto__.nickName = 'lucifer';
console.log(obj); // -> {}
console.log(obj.nickName); // -> lucifer

new

其实继承和原型这部分知识和new是强相关的。 我们有必要了解一下new的原理。

new 的原理很简单, 就是引擎内部新建一个空对象,然后将这个空对象的__proto__ 指向构造函数的prototype.然后调用构造函数,去填充我们创建的空对象(如果有必要)。 最后将this指向我们刚刚创建的新对象。

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 将该对象的__proto__指向构造函数原型;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this(新创建的对象)。
1
2
3
4
5
6
7
8
9
10
11
12
function new_object() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型 (不推荐使用)
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}

__proto__已废弃: 该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。

Object.setPrototypeOf(obj, proto);直接修改已有对象的原型,非常耗时。

警告: 通过现代浏览器的操作属性的便利性,可以改变一个对象的 [[Prototype]] 属性, 这种行为在每一个JavaScript引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的,并且性能消耗的时间也不是简单的花费在 obj.__proto__ = ... 语句上, 它还会影响到所有继承来自该 [[Prototype]] 的对象,如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]。相反, 创建一个新的且可以继承 [[Prototype]] 的对象,推荐使用 Object.create()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 优化后 new 实现
function create() {
// 1、通过call,this指向了arguments(封装实参的对象),shift删除并获得arguments中第一个参数——构造函数
Con = [].shift.call(arguments);
// 2、创建一个空的对象并将该对象的__proto__指向构造函数原型
let obj = Object.create(Con.prototype);
// 3、绑定 this 实现继承,obj 可以访问到构造函数中的属性
let ret = Con.apply(obj, arguments);
// 4、优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
};
function Car(color) {
this.color = color;
}
Car.prototype.start = function () {
console.log(this.color + " car start");
}
let car = create(Car,"black");

补充:

shift/unshift 方法并不局限于数组:这个方法能够通过 call()apply()方法作用于类似数组的对象上。

instanceof

  1. instanceof 判断对象的原型链上是否存在构造函数的原型。只能判断引用类型。
  2. instanceof 常用来判断 A 是否为 B 的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// instanceof 的内部实现 
const instance_of = (Case,Constructor) =>{
// 基本数据类型返回false
// 兼容一下函数对象
if((typeof(Case) !== 'object' && typeof(Case) !== 'function') || Case === null){
return false;
}
let CaseProto = Object.getPrototypeOf(Case);
// let CaseProto = Case.__proto__;
while(true){
// 查到原型链顶端,仍未查到,返回false
if (CaseProto === null) return false;
// 找到相同的原型
if (CaseProto === Constructor.prototype) return true;
// CaseProto = CaseProto.__proto__;
CaseProto = Object.getPrototypeOf(CaseProto);

}
}
let a={};
console.log(instance_of(a,Object))

Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)。

call/apply

Function.prototype.call()

call() 方法调用一个函数, 其具有一个指定的 this 值和多个参数(参数的列表)。

1
func.call(thisArg, arg1, arg2, ...)

它运行 func,提供的第一个参数 thisArg 作为 this,后面的作为参数。

看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function sayWord() {
var talk = [this.name, 'say', this.word].join(' ');
console.log(talk);
}

var bottle = {
name: 'bottle',
word: 'hello'
};

// 使用 call 将 bottle 传递为 sayWord 的 this
sayWord.call(bottle);
// bottle say hello

所以,call 主要实现了以下两个功能:

  • call 改变了 this 的指向
  • bottle 执行了 sayWord 函数

上面代码等效于

1
2
3
4
5
6
7
8
9
10
11
var bottle = {
name: 'bottle',
word: 'hello',
sayWord:function() {
var talk = [this.name, 'say', this.word].join(' ');
console.log(talk);
}
};

bottle.sayWord();
// bottle say hello

这里把函数作为对象的属性存在,通过对象属性执行函数时,相当于对this进行了隐式绑定

模拟实现 call

模拟实现 call 有三步:

  • 将函数设置为绑定对象的属性
  • 执行函数
  • 删除对象的这个属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Function.prototype.newCall = function (context) {

// 注意:非严格模式下,
// 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中就是 window 对象)
// 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象(用 Object() 转换)
context = context ? Object(context) : window;
// 将函数设为对象的属性
// newCall由函数调用,此时的this指向函数,是一种隐式绑定
context.fn = this;

// 执行该函数
const args = [...arguments].slice(1);
const result = context.fn(...args);

// 删除该函数
delete context.fn
// 注意:函数是可以有返回值的
return result;
}

补充

封装实参的对象 arguments

  • arguments是一个类数组对象,它也可以通过索引来操作数据,也可以获取长度。
  • 在调用函数时,我们所传递的实参都会在arguments中保存
  • arguments.length可以用来获取实参的长度
  • arguments[0] 表示第一个实参,arguments[1] 表示第二个实参 。。。

展开语法、剩余参数

  • 展开语法(扩展运算符)是将数组或者可迭代对象拆分成逗号分隔的参数序列。

  • 剩余参数语法允许我们将一个不定数量的参数表示为一个数组。

  • 剩余语法(Rest syntax) 看起来和展开语法完全相同,不同点在于, 剩余参数用于解构数组和对象。从某种意义上说,剩余语法与展开语法是相反的:展开语法将数组展开为其中的各个元素,而剩余语法则是将多个元素收集起来并“凝聚”为单个元素。

slice()

  • slice() 方法返回一个新的数组对象,这一对象是一个由 beginend 决定的原数组的浅拷贝(包括 begin,不包括end)。
  • 原始数组不会被改变。

Function.prototype.apply()

apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或[类似数组对象)提供的参数。

1
func.apply(thisArg, [argsArray])

它运行 func 设置 this = context 并使用类数组对象 args 作为参数列表。

callapply 之间唯一的语法区别是 call 接受一个参数列表,而 apply 则接受带有一个类数组对象。

需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。

模拟实现 apply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.newApply = function (context, arr) {
// 注意:非严格模式下,
// 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中就是 window 对象)
// 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象(用 Object() 转换)
context = context ? Object(context) : window;
// 将函数设为对象的属性
context.fn = this;
// 执行该函数
const result = arr ? context.fn(...arr):context.fn();
// 删除该函数
delete context.fn
// 注意:函数是可以有返回值的
return result;
}

bind

Function.prototype.bind()

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。

来个例子说明下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let value = 2;
let foo = {
value: 1
};
function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
};

bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}

let bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}

let bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}

通过上述代码可以看出 bind 有如下特性:

  • 指定 this
  • 传入参数
  • 返回一个函数
  • 柯里化

bind 还有一个特性:

一个绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

来个例子说明下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let value = 2;
let foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';

let bindFoo = bar.bind(foo, 'Jack');
let obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin

上面例子中,运行结果 this.value 输出为 undefined ,这不是全局 value 也不是 foo 对象中的 value ,这说明 bindthis 对象失效了,new 的实现中生成一个新的对象,这个时候的 this 指向的是 obj

模拟实现bind

大体思路:

  • 拷贝源函数:

    • 通过变量储存源函数 const self = this; this 指向源函数

    • 使用空对象或Object.create复制源函数的prototype给返回函数

  • 返回拷贝的函数

  • 调用拷贝的函数:

    • new调用判断:通过instanceof判断函数是否通过new调用,来决定绑定的context

    • 绑定this+传递参数 apply

    • 返回源函数的执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Function.prototype.newBind = function (context) {
// 调用 bind 的不是函数,需要抛出异常
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

// this 指向调用者
const self = this;
// 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
const args = Array.prototype.slice.call(arguments, 1);

// 创建一个空对象
const fNOP = function () {};

// 实现第3点,返回一个函数
const fBound = function () {
// 实现第4点,获取 bind 返回函数的参数
const bindArgs = [].slice.call(arguments);
// 然后同传入参数合并成一个参数数组,并作为 self.apply() 的第二个参数
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
// 当作为构造函数时,`this` 指向实例,此时 `this instanceof fBound` 结果为 `true`
// 可以让实例获得来自绑定函数的值
// 当作为普通函数时,`this` 指向 `window` ,此时结果为 `false`
// 将绑定函数的 `this` 指向 `context`
}

// 空对象的原型指向绑定函数的原型
fNOP.prototype = self.prototype;
// 空对象的实例赋值给返回函数 实例就可以继承绑定函数的原型中的值
fBound.prototype = new fNOP();
return fBound;
}

至于为什么使用一个空对象 fNOP 作为中介,把 fBound.prototype 赋值为空对象的实例(原型式继承),这是因为直接 fBound.prototype = this.prototype 有一个缺点,修改 fBound.prototype 的时候,也会直接修改 this.prototype ;其实也可以直接使用ES5的 Object.create() 方法生成一个新对象,但 bindObject.create() 都是ES5方法,部分IE浏览器(IE < 9)并不支持。

1
2
3
4
if (self.prototype) {
// 复制源函数的prototype给fBound 一些情况下函数没有prototype,比如箭头函数
fBound.prototype=Object.create(self.prototype) ;
}

注意: bind() 函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8 及以下的版本中不被支持。

补充:

类数组->数组

slice 方法可以用来将一个类数组(Array-like)对象/集合转换成一个新数组。你只需将该方法绑定到这个对象上。

除了使用 Array.prototype.slice.call(arguments),你也可以简单的使用 [].slice.call(arguments) 来代替。

柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var add = function(x) {
return function(y) {
return x + y;
};
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。所以说 bind 本身也是闭包的一种使用场景。

柯里化是将 f(a,b,c) 可以被以 f(a)(b)(c) 的形式被调用的转化。JavaScript 实现版本通常保留函数被正常调用和在参数数量不够的情况下返回偏函数这两个特性。

继承方法

原型链继承

构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。

继承的本质就是复制,即重写原型对象,代之以一个新类型的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType() {
this.property = true;
}

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

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

// 这里是关键,创建SuperType的实例,并将该实例赋值给SubType.prototype
SubType.prototype = new SuperType();

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

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

img

原型链方案存在的缺点:多个实例对引用类型的操作会被篡改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){}

SubType.prototype = new SuperType();

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

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

借用构造函数

使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)

1
2
3
4
5
6
7
8
9
10
11
12
13
function  SuperType(){
this.color=["red","green","blue"];
}
function SubType(){
//继承自SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,green,blue,black"

var instance2 = new SubType();
alert(instance2.color);//"red,green,blue"

核心代码是SuperType.call(this),创建子类实例时调用SuperType构造函数,于是SubType的每个实例都会将SuperType中的属性复制一份。

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

组合继承

组合上述两种方法就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};

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

// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType();
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

缺点:

  • 第一次调用SuperType():给SubType.prototype写入两个属性namecolor
  • 第二次调用SuperType():给instance1写入两个属性namecolor

实例对象instance1上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型

1
2
3
4
5
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}

object()对传入其中的对象执行了一次浅复制将构造函数F的原型直接指向传入的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

缺点:

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

另外,ES5中存在Object.create()的方法,能够代替上面的object方法。

寄生式继承

核心:在原型式继承的基础上,增强对象,返回构造函数

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

函数的主要作用是为构造函数新增属性和方法,以增强函数

1
2
3
4
5
6
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

缺点(同原型式继承):

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

寄生组合式继承

结合借用构造函数传递参数和寄生模式实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function(){
alert(this.age);
}

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]

img

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()

这是最成熟的方法,也是现在库实现的方法

混入方式继承多个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
// do something
};

Object.assign会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。

ES6类继承extends

extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法,使用例子如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Rectangle {
// constructor
constructor(height, width) {
this.height = height;
this.width = width;
}

// Getter
get area() {
return this.calcArea()
}

// Method
calcArea() {
return this.height * this.width;
}
}

const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200

-----------------------------------------------------------------
// 继承
class Square extends Rectangle {

constructor(length) {
super(length, length);

// 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
this.name = 'Square';
}

get area() {
return this.height * this.width;
}
}

const square = new Square(10);
console.log(square.area);
// 输出 100

extends继承的核心代码如下,其实现和上述的寄生组合式继承方式一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function _inherits(subType, superType) {

// 创建对象,创建父类原型的一个副本
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
// 指定对象,将新创建的对象赋值给子类的原型
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});

if (superType) {
Object.setPrototypeOf
? Object.setPrototypeOf(subType, superType)
: subType.__proto__ = superType;
}
}

总结

1、函数声明和类声明的区别

函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则会抛出一个ReferenceError

2、ES5继承和ES6继承的区别

  • ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this))
  • ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。