持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
题目:解释一下什么是原型链?
老生常谈的问题,记得从我刚毕业准备面试时这个题目就已经在准备清单上了。从那时候观摩答案,到现在,我应该如何准备这道题。 回归本质,面试官问这个问题,其根本目的想考察什么? 可能是从网上仿照下来的问题,照猫画虎的问。那么你只需要把网上的答案背下来即好。因为他可能也没想好你能回答出来其他的。 嘿嘿,当然不同的人有不同的观点,我想也许他想考察的是 JS 里面的继承关系,那么就以这个方向回答一番。
作为一个争议不断的新类语言,JS 有很多名称相似却实现方法却截然不同的语法,举个栗子,那就说继承吧。 JS 中的继承原理是原型链继承(先把定义写上,后面描述为什么这么说),那么原型链是什么:
// 首先我们先编写一车构造函数
function Car(color, price) {
this.color = color;
this.price = price;
}
// 给车加一个获取报价的方法
Car.prototype.getPrice = () => {
return this.price
}
Car.prototype.name = '比亚迪'
// 根据这个构造函数创建两个实例car1,car2
let car1 = new Car('red', 1000000)
let car2 = new Car('green', 1000000)
现在看下car1,如下图,它拥有了Car的属性和方法,并且有自己的变量color和price car2 相同,他们所涉及的属性和方法是一样的,而自身的变量属性值是不同的。 改变Car的属性,会发现car1,car2会同步更新,那么说明car1、car2属性所指向的应该是同一个属性 根据现在的状况,画张图说明:
- Car 为构造函数
- prototype 为函数对象的特有属性
- Car 的原型是 Function.prototype
- car1 为 Car 的实例,car1 的原型通过 proto属性获取,原型指向就是 Car.prototype
- car2 同理
原型链 概念:实例 => 实例.原型 => 实例.原型.原型 => ... => (Object.prototype).原型 => null。 这一条链路就是一个实例的原型链,原型链的终点是 Object 的原型,Object原型后就无原型,为null。 回归上图,绿色为Car的原型链,蓝色线为car2的原型链,虚线为car1的原型链,可以看到car1和car2都为同一个构造函数的实例,所以原型链开头虽然来自不同的起点,但其终点指向Object.prototype的线是重叠的(蓝色且虚线)。 了解原型链,通往继承的距离又近了一步,先了解下 class 关键字。在ES6中引入了class的语法糖,使 JS 在书写形式上更接近面向对象语言的写法。先简单铺写这个与语法糖的使用:
class Car {
constructor(color, price) {
this.color = color;
this.price = price;
}
name = '比亚迪';
getPrice() {
return this.price;
}
}
let car1 = new Car('red', 1000000)
let car2 = new Car('green', 1000000)
为了有对照关系,我使用class语法复写了上面的栗子。对照下来结论:
- class Car 里面的 constructor 相等于上面的构造函数 Car(划个重点:只是实现上视觉上的相等)。可以试验下,Car.prototype.constructor 是否等于 Car.constructor ?结论是不相等,构造函数就是Car类本身,而我们要获取的 Car.constructor 也不是我们想看到的函数,而是Function构造函数。截图如下:
- class中定义的函数getPrice,其实是定义在了类的原型上,相等于上面栗子的 Car.prototype.getPrice。
- class中的name、color、price,创建实例后,是定义在实例本身上。也可以通过这个得出constructor里面的this是指向实例本身的。
目前,我们提到了构造函数、实例、原型、原型链、类(class),我们可以开始看继承这个问题了。class 提供了 extends 特性,可通过该关键字,继承任何拥有 [[Constructor]] 和原型的对象。简单使用下:
class CarType {
constructor(price) {
this.price = price;
this.color = ['red', 'green']
}
consolePrice() {
console.log(this.price)
}
}
CarType.prototype.type = 'car'
class Car extends CarType {
constructor(price, nameplate) {
super(price)
this.nameplate = nameplate
}
consoleNameplate() {
console.log(this.nameplate)
}
}
let car1 = new Car(10000, '别克')
let car2 = new Car(20000, '比亚迪')
创建好,类与实例,我们尝试使用一下,如下图: 可以看到,car1可以成功获取type属性的值,但是从输入信息可以看到car1本身是没有这个属性的,那么我们就一层一层上去搜索一下,最后在car1.proto.proto中拿取 我们来画下原型图,梳理下: 发现car1之所以可以拿到type属性,其根本还是通过原型链(紫色),一层一层搜索到的。得出结论:class和其extends的实现,它背后使用的仍是原型和构造函数的概念。
这个是ES6引进的class和extends语法糖结构,那么ES6之前就没有继承吗?根据上面得到的结论,我们就可知道class和extends的原理是在ES6之前就存在的,那么ES6之前肯定就存在继承,只是写法更为原始。以上面画的原型图,我们根据其原型和构造函数的关系,用非class的语法实现一下:
// CarType 的构造函数,此时:
// CarType.prototype = CarType.prototype
// CarType.prototype.constructor = CarType
function CarType(price) {
this.price = price;
this.color = ['red', 'green'];
}
// 创建定义在构造函数原型的函数
CarType.prototype.consolePrice = () => {
console.log(this.price)
}
// Car 的构造函数,此时:
// Car.prototype = Car.prototype
// Car.prototype.constructor = Car
function Car(price, nameplate) {
CarType.call(this, price);
this.nameplate = nameplate;
}
// 根据上面的原型图,我们需要把Car.prototype.__proto__指向CarType.prototype,那么
// 我们根据CarType.prototype创建一个副本赋给Car.prototype
// 正时候我们就需要用到Object.create方法
// 顺带提下Object.create的原理
// Object.create = function (obj) {
// function F() {}
// F.prototype = obj;
// return new F();
// };
Car.prototype = Object.create(CarType.prototype);
// 由上步执行后,此时状态:
// Car.prototype = 我们新创建的CarType.prototype副本
// Car.prototype.__proto__ = CarType.prototype
// Car.prototype.constructor = CarType
// 这时发现,重写prototype属性导致Car.prototype.constructor的指向错误,所以我们需要更正回来
Car.prototype.constructor = Car;
// 这时候再向Car的原型定义函数、变量等
// 如果构造完构造函数就像向Car的原型定义函数、变量,后面的prototype重新指向会将新原型覆盖老原型,导致函数变量的缺失
Car.prototype.consoleNameplate = () => {
console.log(this.nameplate)
}
let car1 = new Car(10000, '别克')
let car2 = new Car(20000, '比亚迪')
实现完,我们会发现这就是JS的寄生式组合继承。
寄生式组合继承被认为式实现基于类型继承的最有效方式。—— 《JavaScript高级程序设计》
由原型链为引子,啰啰嗦嗦写了一大堆。编程就是个很有意思的领域,从会用开始,不断会有新的理解,新的感悟。