一、ES5中的继承
1. 原型链
-
原型链
-
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
-
原型对象:每个构造函数在创建时都会有一个
prototype
属性指向这个函数的原型对象,而原型对象会获得一个constructor
属性指向构造函数。当调用构造函数创建实例后,实例都包含一个指向构造函数的原型对象(不是指向构造函数)的内部指针,Firefox、Safiri、Chrome用__proto__
表示这个指针。 -
原型链:如果一个实例的原型对象等于另一个类型的实例,而那个实例的原型对象又等于其他类型的实例,如此层层递进,就构成了实例与原型的链条,这就是所谓的原型链。
function A() {} function B() {} B.prototype = new A(); var c = new B();
-
原型搜索机制:读取一个实例属性时,首先会在事例中搜索该属性。如果没有找到,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程得以沿着原型链继续向上。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端(
Object.prototype
)才会停下来。
-
-
确定原型与实例之间的关系
-
instanceof
:测试实例与构造函数c instanceof B // true c instanceof A // true c instanceof Object // true
-
isPrototypeOf()
:测试实例与原型B.prototype.isPrototypeOf(c) // true A.prototype.isPrototypeOf(c) // true Object.prototype.isPrototypeOf(c) // true
-
-
谨慎定义方法
- 子类型给原型添加方法的代码一定要放在替换原型的语句之后。如:
- 重写超类型中的某个方法;
- 添加超类型中不存在的某个方法。
- 在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。
- 子类型给原型添加方法的代码一定要放在替换原型的语句之后。如:
-
原型链的问题:
- 包含引用类型值的原型,所有实例属性会共享该引用类型的属性。
- 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下 ,向超类型的构造函数中传递参数 。
2. 借用构造函数(伪造对象或经典继承)
function A() {}
function B() {
A.call(this);
}
var c = new B();
- 基本思想:
- 在子类型构造函数的内部调用超类型构造函数。
- 优势:
- 可以在子类型构造函数中向超类型构造函数传递参数。
A.call(this, name, sex, ...)
- 可以在子类型构造函数中向超类型构造函数传递参数。
- 问题:
- 方法在构造函数中定义,无法实现函数复用。
- 在超类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。
3. 组合继承(伪经典继承)
function A() {}
function B() {
// 继承属性
A.call(this, name, ...);
}
// 继承方法
B.prototype = new A();
B.prototype.constructor = B;
- 基本思想:
- 使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。
- 优点:
- 既实现了函数复用,又保证了每个实例都有自己的属性。
- 可以使用
instanceof
和isPrototypeOf()
识别基于组合继承创建的对象。 - 是
JavaScript
中最常用的继承模式。
4. 原型式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var a = {};
var b = object(a);
- 基本思想:
- 借助原型基于已有的对象创建新对象。
- 本质:
object()
对传入其中的对象执行了一次浅复制。
- ES5通过新增
Object.create()
方法规范了原型式继承:- 这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
- 第二个参数定义的属性会覆盖原型对象上的同名属性。
- 缺点:
- 和使用原型模式一样,包含引用类型值的属性始终会共享相应的值。
5. 寄生式继承
function A(original) {
var clone = object(original);
clone.sayHi = function() {
alert('Hi');
};
return clone;
}
var a = {};
var b = A(a);
- 基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
- 适用场景:主要考虑对象而不是自定义类型和构造函数的情况。
6. 寄生组合式继承
-
组合继承的不足:无论什么情况下,都会调用两次超类型构造函数,一次是创建子类型原型的时候,另一次是子类型构造函数内部。
-
寄生组合式继承的基本模式:
function inherite(subType, superType) { var f = object(superType.prototype); // 创建superType的实例f f.constructor = subType; subType.prototype = f; } function SuperType() {} function SubType() { SuperType.call(this); } inherite(SubType, SuperType);
-
基本思想:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。实际上是在组合继承的基础上,用超类型原型的副本代替调用超类型的构造函数给子类型指定原型。
-
本质上,是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
-
优点:只调用了一次超类型的构造函数,并且因此避免了在子类型的原型上创建不必要的、多余的属性。与此同时,原型链还能保持不变。因此,还能够正常使用
instanceof
和isPrototypeOf()
。普遍认为寄生组合式继承是引用类型最理想的继承方式。
二、new操作符
1. 使用new操作符调用构造函数
- 创建一个新对象;
- 将新对象链接到构造函数的原型上;
- 将构造函数的作用域赋给新对象(绑定this);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
2. new操作符的实现
function create() {
let obj = {}; // 创建一个新对象
let Con = [].shift.call(arguments); // 获取构造函数
obj.__proto__ = Con.prototype; // 链接到原型
let result = Con.apply(obj, arguments); // 绑定this,执行构造函数
return typeof result === 'object' ? result : obj; // 确保new出来的是个对象
}
// 调用
function F() {}
var f = create(F);
3. new操作符的优先级问题
function Foo() {
return this;
}
Foo.getName = function() {
console.log(1);
}
Foo.prototype.getName = function() {
console.log(2);
}
代码 | 执行结果 | 执行过程 |
---|---|---|
new Foo new Foo() |
Foo {} |
|
new Foo.getName new (Foo.getName) new Foo.getName() new (Foo.getName)() |
1 Foo.getName {} |
1. 执行var f = Foo.getName 2. 执行 new f 或new f() |
(new Foo).getName (new Foo()).getName new Foo().getName |
f() { console.log(2); } |
1. 执行var f = new Foo() 2. 访问 f.getName |
(new Foo).getName() (new Foo()).getName() new Foo().getName() |
2 |
1. 执行var f = new Foo() 2. 调用 f.getName() |
优先级:.
与()
相等,大于new
操作符的优先级
三、ES6继承
1. Class
-
Class可看作构造函数的另一种写法
- 类的数据类型是函数,类本身指向构造函数
- 类的所有方法都定义在类的
prototype
属性上面- 在类的实例上调用方法,其实就是调用原型上的方法
- 可以通过实例的
__proto__
属性为类添加方法,但是不推荐使用,会影响到所有实例
- 类的内部定义的所有方法,都是不可枚举的(与 ES5 行为不一致)
class Point { constructor() {} toString() {} } typeof Point // "function" Point === Point.prototype.constructor // true var b = new Point(); // 使用new操作符创建实例 b.constructor === Point.prototype.constructor // true b.constructor === b.__proto__.constructor // true Object.keys(Point.prototype) // [] Object.getOwnPropertyNames(Point.prototype) // ["constructor", "toString"] var Spot = function() {} // 使用ES5改写Class Spot.prototype.toString = function() {} Object.keys(Spot.prototype) // ["toString"] Object.getOwnPropertyNames(Spot.prototype) // ["constructor", "toString"]
-
注意点
- 类和模块的内部,默认是严格模式
- 类不存在变量提升,声明前使用会报错
- 类的方法内部如果含有
this
,它默认指向类的实例- 将类的方法提取出来单独使用时,方法内的
this
会指向该方法运行时所在的环境 => 可以在构造函数中使用bind
或箭头函数让this
指向实例对象
- 将类的方法提取出来单独使用时,方法内的
-
其他特性
- 静态方法:使用
static
修饰,表示该方法不会被实例继承,而是直接通过类来调用。- 静态方法中的
this
指的是类,不是实例。 - 父类的静态方法,可以被子类继承。
- 静态方法中的
- 实例属性:可以定义在
constructor()
方法里的this
上面,也可以定义在类的最顶层。 new.target
:用在构造函数或Class
中,返回new
命令作用于的那个构造函数或Class
。- 如果构造函数不是通过
new
命令或Reflect.construct()
调用的,new.target
会返回undefined
。 - 在函数外部使用
new.target
会报错。
- 如果构造函数不是通过
- 静态方法:使用
2. Class的继承
-
使用
extends
实现继承,子类继承了父类的所有属性和方法。- 子类必须在
constructor
方法中调用super
方法,否则新建实例时会报错。 - 在子类的构造函数中,只有调用
super
之后,才能使用this
关键字。
- 子类必须在
-
与ES5继承机制的区别:
-
ES5的继承,实质是先创造子类的实例对象
this
,然后再将父类的方法添加到this
上面。Parent.apply(this)
-
ES6的继承,实质是先通过
super()
方法将父类实例对象的属性和方法,加到this
上面,然后再用子类的构造函数修改this
。
-
-
super
关键字:既可以当做函数使用,也可以当作对象使用-
super
作为函数调用时,代表父类的构造函数,返回子类的实例。只能用在子类的构造函数中,用在其他地方会报错。class A {} class B extends A { constructor() { super(); // 相当于 A.prototype.constructor.call(this) } }
-
super
作为对象使用时:(1) 在子类的普通方法中通过
super
调用父类的方法时,super
指向父类的原型对象- 定义在父类实例上的方法或属性,无法通过
super
调用 - 父类方法内部的
this
指向当前的子类实例 - 通过
super
对某个属性赋值时,super
就是this
,赋值的属性会变成子类实例的属性 - 通过
super
访问某个属性时,super
依旧指向父类的原型对象
(2) 在子类的静态方法中通过
super
调用父类的方法时,super
指向父类- 父类方法内部的
this
指向当前的子类
- 定义在父类实例上的方法或属性,无法通过
-
使用
super
的时候,必须显示指定是作为函数还是作为对象使用,否则会报错,如:console.log(super); // 报错
-
由于对象总是继承自其他对象,所以可以在任何一个对象中,使用
super
关键字
-
-
Class的
prototype
属性和__proto__
属性-
大多数浏览器的ES5实现中,每一个对象都有
__proto__
属性,指向对应的构造函数的prototype
属性。 -
ES6的Class中,同时有
prototype
属性和__proto__
属性,因此同时存在两条继承链:- 子类的
__proto__
属性指向父类 - 子类
prototype
属性的__proto__
属性指向父类的prototype
属性
class A {} class B extends A {} B.__proto__ = A // true B.prototype.__proto__ = A.prototype // true
- 子类的
-
原因,类的继承是按照下面的模式实现的:
class A {} class B {} // B的实例继承A的实例 Object.setPrototypeOf(B.prototype, A.prototype); // let a = new A(); // let b = new B(); // B.prototype === b.__proto__ // A.prototype === a.__proto__ // B继承A的静态属性 Object.setPrototypeOf(B, A); // Object.setPrototypeOf的实现 Object.setPrototypeOf = function(obj, proto) { obj.__proto__ = proto; return obj; }
-
-
原生构造函数的继承
- ES5中,无法继承原生构造函数。原因是ES5先构建子类的实例对象
this
,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。 - ES6允许继承原生构造函数定义子类,因为ES6先构建父类的实例对象
this
,然后再用子类的构造函数修饰this
,使得父类的所有行为都可以继承。
- ES5中,无法继承原生构造函数。原因是ES5先构建子类的实例对象
参考:
- JavaScript高级程序设计(第三版),第6章。
- 前端进阶之道:yuchengkai.cn/docs/fronte…
- 【阮一峰】ES6入门:es6.ruanyifeng.com/#docs/class