继承于我们前端来说绝对是非常熟悉也必须熟悉的一个高频必懂知识点。熟悉到只要是面试一定会有关于继承的问题;而且源码中继承的使用也随处可见。
可依旧有很多前端对继承的实现和应用没有一个整体的把握。追其原因无非有二:
无论由于哪一个原因,建议请尽快弄懂继承的实现和应用,否则你可能会如同你的表情包一样——流下了没有技术的泪水。
接下来我会尽我所能讲清楚继承这个概念,并结合相关经典图文做辅助解释。
在讲 ECMAScript 继承的概念之前,我先说下类和原型的概念。
讲 ECMAScript 继承的概念之前,我先说下类的概念。(如果接触过 Java 或者是 C++ 的话,我们就知道 Java(C++)的继承都是基于类的继承)。
类: 是面向对象(Object Oriented)语言实现信息封装的基础,称为类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。 类: 是描述了一种代码的组织结构形式,一种在软件中对真实世界中问题领域的建模方法。
类的概念这里我就不再扩展,感兴趣的同学可以自行查阅书籍。接下来我们重点讲讲原型以及原型链。
JavaScript 这门语言没有类的概念,所以 JavaScript 并非是基于类的继承,而是基于原型的继承。(主要是借鉴 Self 语言原型( prototype
)继承机制)。
注意:ES6 中的 class 关键字和 OO 语言中的类的概念是不同的,下面我会讲到。ES6 的 class 其内部同样是基于原型实现的继承。
JavaScript 摒弃类转而使用原型作为实现继承的基础,是因为基于原型的继承相比基于类的继承上在概念上更为简单。首先我们明确一点,类存在的目的是为了实例化对象,而 JavaScript 可以直接通过对象字面量语法轻松的创建对象。
每一个函数,都有一个 prototype
属性。 所有通过函数 new
出来的对象,这个对象都有一个 __proto__
指向这个函数的 prototype
。 当你想要使用一个对象(或者一个数组)的某个功能时:如果该对象本身具有这个功能,则直接使用;如果该对象本身没有这个功能,则去 __proto__
中找。
prototype
[显式原型]
prototype
是一个显式的原型属性,只有函数才拥有该属性。 每一个函数在创建之后都会拥有一个名为prototype
的属性,这个属性指向函数的原型对象。( 通过Function.prototype.bind
方法构造出来的函数是个例外,它没有prototype
属性 )。
prototype
是一个指针,指向的是一个对象。比如 Array.prototype
指向的就是 Array 这个函数的原型对象。
在控制台中打印 console.log(Array.prototype)
里面有很多方法。这些方法都以事先内置在 JavaScript 中,直接调用即可。上面我标红了两个特别的属性 constructor
和 __proto__
。这两个属性接下来我都会讲。
我们现在写一个 functionnoWork(){}
函数。
当我写了一个 noWork
这个方法的时候,它自动创建了一个 prototype
指针属性(指向原型对象)。而这个被指向的原型对象自动获得了一个 constructor
(构造函数)。细心的同学一定发现了: constructor
指向的是 noWork
。
noWork.prototype.constructor === noWork // true
// 一个函数的原型对象的构造函数是这个函数本身
tips: 图中打印的 Array 的显式原型对象中的这些方法你都知道吗?要知道数组也是非常重要的一部分哦 ~ 咳咳咳,这是考试重点。
__proto__
[隐式原型]prototype
理解起来不难, __proto__
理解起来就会比 prototype
稍微复杂一点。不过当你理解的时候你会发现,这个过程真的很有趣。下面我们就讲讲 __proto__
。
其实这个属性指向了 `[[prototype]]`,但是 `[[prototype]]` 是内部属性,我们并不能访问到,所以使用 `__proto__` 来访问。
我先给个有点绕的定义:
__proto__
指向了创建该对象的构造函数的显式原型。
我们现在还是使用 noWork
这个例子来说。我们发现 noWork
原型对象中还有另一个属性 __proto__
。
我们先打印这个属性:
我们发现这个 __proto__
指向的是 Object.prototype
。
我听到有人在问为什么?
__proto__.constructor
指向的是 Object
。__proto__.constructor
指向的是 Object.prototype.constructor
。__proto__
指向的是 Object.prototype
。
至于为什么是指向 Object? 因为所有的引用类型默认都是继承 Object 。
作用
noWork
这个对象中的 toString()
属性时,在 noWork
中找不到,就会沿着 __proto__
依次查找。当我们使用 new 操作符时,生成的实例对象拥有了 __proto__
属性。即在 new 的过程中,新对象被添加了 __proto__
并且链接到构造函数的原型上。
new 的过程
Function.__proto__===Function.prototype
难道这代表着 Function
自己产生了自己? 要说明这个问题我们先从 Object
说起。
我们知道所有对象都可以通过原型链最终找到 Object.prototype
,虽然 Object.prototype
也是一个对象,但是这个对象却不是 Object
创造的,而是引擎自己创建了 Object.prototype
。 所以可以这样说:
所有实例都是对象,但是对象不一定都是实例。
接下来我们来看 Function.prototype
这个特殊的对象:
打印这个对象,会发现这个对象其实是一个函数。我们知道函数都是通过 newFunction()
生成的,难道 Function.prototype
也是通过 newFunction()
产生的吗?这个函数也是引擎自己创建的。
首先引擎创建了
Object.prototype
,然后创建了Function.prototype
,并且通过__proto__
将两者联系了起来。
这就是为什么 Function.prototype.bind()
没有 prototype
属性。因为 Function.prototype
是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype
属性。
对于为什么
Function.__proto__
会等于Function.prototype
? 我看到的一个解释是这样的: 其他所有的构造函数都可以通过原型链找到Function.prototype
,并且functionFunction()
本质也是一个函数,为了不产生混乱就将functionFunction()
的__proto__
联系到了Function.prototype
上。
在 JavaScript
中继承是非常重要的一个概念。我们有必要去了解,请大家多指教。
目的:简化代码逻辑和结构,实现代码重用
接下来我们一起学习下 8 种 JavaScript
实现继承的方法。
推荐组合继承(四)、寄生组合式继承(七)、ES6 继承(八)
基本思想是利用原型让一个引用类型继承另一个引用类型的方法和实例。
function staff(){ this.company = 'ABC';}staff.prototype.companyName = function(){ return this.company; }function employee(name,profession){ this.employeeName = name; this.profession = profession;}// 继承 staffemployee.prototype = new staff();// 将这个对象的 constructor 手动改成 employee,否则还会是 staffemployee.prototype.constructor = employee;// 不使用对象字面量方式创建原型方法,会重写原型链employee.prototype.showInfo = function(){ return this.employeeName + "'s profession is " + this.profession;}let instance = new employee('Andy','front-end');
// 测试 console.log(instance.companyName()); // ABCconsole.log(instance.showInfo()); // "Andy's profession is front-end"// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instance.hasOwnProperty('employeeName')) // trueconsole.log(instance.hasOwnProperty('company')) // false// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(employee.prototype.isPrototypeOf(instance)); // trueconsole.log(staff.prototype.isPrototypeOf(instance)); // trueconsole.log(Object.prototype.isPrototypeOf(instance)); // true
原型链实现继承最大的问题是:
当原型中存在引用类型值时,实例可以修改其值。
function staff(){ this.test = [1,2,3,4];}function employee(name,profession){ this.employeeName = name; this.profession = profession;}employee.prototype = new staff();let instanceOne = new employee();let instanceTwo = new employee();instanceOne.test.push(5);console.log(instanceTwo.test); // [1, 2, 3, 4, 5]
鉴于此问题:所以我们在实践中会少单独使用原型链实现继承。
hasOwnProperty()
方法来确定自身属性与其原型属性isPrototypeOf()
方法来确定原型和实例的关系此方法和方法一区别就是将:
employee.prototype = new staff();
改成:
Employee.prototype = Person.prototype;
此方法可以解决原型中引用类型值被修改的问题
function staff(){ this.test = [1,2,3];}staff.prototype.companyName = function(){ return this.company; }function employee(name,profession){ staff.call(this); this.employeeName = name; this.profession = profession;}// 不使用对象字面量方式创建原型方法,会重写原型链employee.prototype.showInfo = function(){ return this.employeeName + "'s profession is " + this.profession;}let instanceOne = new employee('Andy','front-end');let instanceTwo = new employee('Mick','after-end');instanceOne.test.push(4);// 测试 console.log(instanceTwo.test); // [1,2,3]// console.log(instanceOne.companyName()); // 报错// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instanceOne.hasOwnProperty('test')) // true// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(staff.prototype.isPrototypeOf(instanceOne)); // false
从上面的结果可以看出:
instanceOne
与 staff
已经没有原型链的关系了指的是将原型链技术和借用构造函数技术结合起来,二者皆取其长处的一种经典继承方式。
function staff(){ this.company = "ABC"; this.test = [1,2,3];}staff.prototype.companyName = function(){ return this.company; }function employee(name,profession){ // 继承属性 staff.call(this); this.employeeName = name; this.profession = profession;}// 继承方法employee.prototype = new staff();employee.prototype.constructor = employee;employee.prototype.showInfo = function(){ return this.employeeName + "'s profession is " + this.profession;}
let instanceOne = new employee('Andy','front-end');let instanceTwo = new employee('Mick','after-end');instanceOne.test.push(4);// 测试 console.log(instanceTwo.test); // [1,2,3]console.log(instanceOne.companyName()); // ABC// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instanceOne.hasOwnProperty('test')) // true// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(staff.prototype.isPrototypeOf(instanceOne)); // true
staff
会被调用 2 次:第 1 次是 employee.prototype=newstaff();
,第 2 次是调用 staff.call(this)
。利用一个临时性的构造函数(空对象)作为中介,将某个对象直接赋值给构造函数的原型。
function object(obj){ function F(){} F.prototype = obj; return new F();}
本质上 object()
对传入其中的对象执行了一次浅复制,将构造函数 F
的原型直接指向传入的对象。
var employee = { test: [1,2,3]}
let instanceOne = object(employee);let instanceTwo = object(employee);// 测试 instanceOne.test.push(4);console.log(instanceTwo.test); // [1, 2, 3, 4]
另,ES5 中存在 Object.create()
的方法规范化了原型式继承,能够代替 object
方法。
要点:在原型式继承的基础上,通过封装继承过程的函数增强对象,返回对象
function createAnother(original){ var clone = object(original); // 通过调用 object() 函数创建一个新对象 clone.sayHi = function(){ // 以某种方式来增强对象 alert("hi"); }; return clone; // 返回这个对象}
createAnother
函数的主要作用是为构造函数新增属性和方法,以增强函数。
该方法主要是解决组合继承调用两次超类构造函数的问题。
function inheritPrototype(sub, super){ var prototype = Object.create(super.prototype); // 创建对象,父原型的副本 prototype.constructor = sub; // 增强对象 sub.prototype = prototype; // 指定对象,赋给子的原型}
function staff(){ this.company = "ABC"; this.test = [1,2,3];}staff.prototype.companyName = function(){ return this.company; }function employee(name,profession){ staff.call(this, name); this.employeeName = name; this.profession = profession;}
// 将父类原型指向子类inheritPrototype(employee,staff)let instanceOne = new employee("Andy", "A");let instanceTwo = new employee("Rose", "B");instanceOne.test.push(4);// 测试 console.log(instanceTwo.test); // [1,2,3]console.log(instanceOne.companyName()); // ABC// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instanceOne.hasOwnProperty('test')) // true// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(staff.prototype.isPrototypeOf(instanceOne)); // true
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式,
Class 可以通过 extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class staff { constructor(){ this.company = "ABC"; this.test = [1,2,3]; } companyName(){ return this.company; }}class employee extends staff { constructor(name,profession){ super(); this.employeeName = name; this.profession = profession; }}
// 将父类原型指向子类let instanceOne = new employee("Andy", "A");let instanceTwo = new employee("Rose", "B");instanceOne.test.push(4);// 测试 console.log(instanceTwo.test); // [1,2,3]console.log(instanceOne.companyName()); // ABC// 通过 Object.getPrototypeOf() 方法可以用来从子类上获取父类console.log(Object.getPrototypeOf(employee) === staff)// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instanceOne.hasOwnProperty('test')) // true// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(staff.prototype.isPrototypeOf(instanceOne)); // true
super
关键字,它在这里表示父类的构造函数,用来新建父类的 this
对象。
constructor
方法中调用 super
方法,否则新建实例时会报错。这是因为子类没有自己的 this
对象,而是继承父类的 this
对象,然后对其进行加工。super
之后,才可以使用 this
关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super
方法才能返回父类实例。
`super` 虽然代表了父类 `A` 的构造函数,但是返回的是子类 `B` 的实例,即` super` 内部的 `this ` 指的是 `B`,因此 `super()` 在这里相当于
A.prototype.constructor.call(this)
ES5 的继承,实质是先创造子类的实例对象 this
,然后再将父类的方法添加到 this
上面( Parent.apply(this)
)。
ES6 的继承机制完全不同,实质是先创造父类的实例对象 this
(所以必须先调用 super()
方法),然后再用子类的构造函数修改 this
。
function _inherits(subType, superType) { 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; }}
由此可以看出:
__proto__
属性,表示构造函数的继承,总是指向父类。prototype
属性的 __proto__
属性,表示方法的继承,总是指向父类的 prototype
属性。
另:ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。
以上八种继承方式是比较常见的继承方式,倘若了解了这些方式的机制,在以后的面试中原型链与继承的问题也就不在话下了。