JS原型与原型链
夏日 人气:0前言
JavaScript
中有许多内置对象,如:Object, Math, Date
等。我们通常会这样使用它们:
// 创建一个JavaScript Date实例 const date = new Date(); // 调用getFullYear方法,返回日期对象对应的年份 date.getFullYear(); // 调用Date的now方法 // 返回自1970-1-1 00:00:00 UTC(世界标准时间)至今所经过的毫秒数 Date.now()
当然,我们也可以自己创建自定义对象:
function Person() { this.name = '张三'; this.age = 18; } Person.prototype.say = function() { console.log('say'); } const person = new Person(); person.name; // 张三 person.say(); // say
看到这些代码,不知道你是否有这些疑问:
new
关键执行函数和普通函数执行有什么区别吗?- 对象的实例为什么可以调用构造函数的原型方法,它们之间有什么关系吗?
接下来,让我们带着这些问题一步步深入学习。
new对函数做了什么?
当我们使用new
关键字执行一个函数时,除了具有函数直接执行的所有特性之外,new
还帮我们做了如下的事情:
- 创建一个空的简单
JavaScript
对象(即{}
) - 将空对象的
__proto__
连接到(赋值为)该函数的prototype
- 将函数的
this
指向新创建的对象 - 函数中如果没有返回对象的话,将
this
作为返回值
用代码表示大概是这样:
// 1. 创建空的简单js对象 const plainObject = {}; // 2. 将空对象的__proto__连接到该函数的prototype plainObject.__proto__ = function.prototype; // 3. 将函数的this指向新创建的对象 this = plainObject; // 4. 返回this return this
可以看到,当我们使用new
执行函数的时候,new
会帮我们在函数内部加工this
,最终将this
作为实例返回给我们,可以方便我们调用其中的属性和方法。
下面,我们尝试实现一下new:
function _new (Constructor, ...args) { // const plainObject = {}; // plainObject.__proto__ = constructor.prototype; // __proto__在有些浏览器中不支持,而且JavaScript也不推荐直接使用该属性 // Object.create: 创建一个新对象,使用现有的对象提供新创建的对象的__proto__ const plainObject = Object.create(Constructor.prototype); // 将this指向新创建的对象 const result = Constructor.call(plainObject, ...args); const isObject = result !== null && typeof result === 'object' || typeof result === 'function'; // 如果返回值不是对象的话,返回this(这里是plainObject) return isObject ? result : plainObject; }
简单用一下我们实现的_new
方法:
function Animal (name) { this.name = name; this.age = 2; } Animal.prototype.say = function () { console.log('say'); }; const animal = new Animal('Panda'); console.log(animal.name); // Panda animal.say(); // say
在介绍new
的时候,我们提到了prototype
,__proto__
这些属性。你可能会疑惑这些属性的具体用途,别急,我们马上进行介绍!
原型和原型链
在学习原型和原型链之前,我们需要首先掌握以下三个属性:
prototype
: 每一个函数都有一个特殊的属性,叫做原型(prototype
)constructor
: 相比于普通对象的属性,prototype
属性本身会有一个属性constructor
,该属性的值为prototype
所在的函数__proto__
: 每一个对象都有一个__proto__
属性,该属性指向对象(实例)所属构造函数(类)的原型prototype
以上的解释只针对于JavaScript
语言
我们再来看下边的一个例子:
function Fn () { this.x = 100; this.y = 200; this.getX = function () { console.log(this.x); }; } Fn.prototype.getX = function () { console.log(this.x); }; Fn.prototype.getY = function () { console.log(this.y); }; const fn = new Fn()
我们画图来描述一下上边代码中实例、构造函数、以及prototype
和__proto__
之间的关系:
我们再来看一下Function
和Object
以及其原型之间的关系:
由于Function
和Object
都是函数,因此它们的所属类为Function
,它们的__proto__
都指向Function.prototype
。而Function.prototype.__proto__
又指向Object.prototype
,所以它们既可以调用函数原型上的方法,也可以调用对象原型上的方法。
当我们需要获取实例上的某个属性时:
上例中:
- 实例:
fn
- 实例所属类:
Fn
- 首先会从自身的私有属性上进行查找
- 如果没有找到,会到自身的
__proto__
上进行查找,而实例的__proto__
指向其所属类的prototype
,即会在类的prototype
上进行查找 - 如果还没有找到,继续到类的
prototype
的__proto__
中查找,即Object.prototype
- 如果在
Object.prototype
中依旧没有找到,那么返回null
上述查找过程便形成了JavaScript
中的原型链。
在理解了原型链和原型的指向关系后,我们看看以下代码会输出什么:
const f1 = new Fn(); const f2 = new Fn(); console.log(f1.getX === f2.getX); console.log(f1.getY === f2.getY); console.log(f1.__proto__.getY === Fn.prototype.getY); console.log(f1.__proto__.getX === f2.getX); console.log(f1.getX === Fn.prototype.getX); console.log(f1.constructor); console.log(Fn.prototype.__proto__.constructor); f1.getX(); f1.__proto__.getX(); f2.getY(); Fn.prototype.getY(); // false // true // true // false // false // Fn // Object // 100 // undefined // 200 // undefined
到这里,我们已经初步理解了原型和原型链的一些相关概念,下面让我们通过一些实际例子来应用一下吧!
借用原型方法
在JavaScript
中,我们可以通过call/bind/apply
来更改函数中this
指向,原型上方法的this
也可以通过这些api
来进行更改。比如我们要将一个伪数组转换为真实数组,可以这样做:
function fn() { return Array.prototype.slice.call(arguments) } fn(1,2,3) // [ 1, 2, 3]
这里我们使用arguments
调用了数组原型上的slice
,这是怎么做到的呢?我们先简单模拟下slice
方法的实现:
arguments
是一个类似数组的对象,有length
属性和从零开始的索引,它可以调用Object.prototype
上的方法,但是不能调用Array.prototype
上的方法。
Array.prototype.mySlice = function (start = 0, end = this.length) { const array = []; // 一般会通过Array的实例(数组)调用该方法,所以this指向调用该方法的数组 // 这里我们将this指向了arguments = {0: 1, 1: 2, 2: 3, length: 3} for (let i = 0; i < end; i++) { array[i] = this[i]; } return array; }; function fn () { return Array.prototype.mySlice.call(arguments); } console.log(fn(1, 2, 3)); // [1, 2, 3]
可能你想直接调用arguments.slice()
方法,但是遗憾的是arguments
是一个对象,不能调用数组原型上的方法。
当我们将Array.prototype.slice
方法的this
指向arguments
对象时,由于arguments
拥有索引属性以及length
属性,所以可以像数组一样根据length
和索引来进行遍历,从而相当于用arguments
调用了数组原型上的方法。
下面是另一个借用原型方法常见的例子:
Object.prototype.toString.call([1,2,3]) // [object Array] Object.prototype.toString.call(function() {}) // [object Number]
这里将Object.prototype.toString
的this
由对象(Object
的实例)改为了数组(Array
的实例)和函数(Function
的实例),相当于为数组和函数调用了对象上的toString
方法,而不是调用它们自身的toString
方法。
通过借用原型方法,我们可以让变量调用自身以及自己原型上没有的方法,增加了代码的灵活性,也避免了一些不必要的重复工作。
实现构造函数之间的继承
通过JavaScript
中的原型和原型链,我们可以实现构造函数的继承关系。假设有如下A
,B
俩个构造函数:
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; function B () { this.b = 200; } B.prototype.getB = function () { console.log(this.b); };
方案一
这里我们可以让B.prototype
成为A
的实例,那么B.prototype
中就拥有了私有方法a
,以及原型对象上的方法B.prototype.__proto__
即A.prototype
上的方法getA
。最后记得要修正B.prototype
的constructor
属性,因为此时它变成了B.prototype.constructor
,也就是B
。
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; B.prototype = new A(); B.prototype.constructor = B; function B () { this.b = 200; } B.prototype.getB = function () { console.log(this.b); };
画图理解一下:
下面我们创建B
的实例,看下是否成功继承了A
中的属性和方法。
const b = new B(); console.log('b', b.a); b.getA(); console.log('b', b.b); b.getB(); // b 100 // 100 // b 200 // 200
方案二
我们也可以通过将父构造函数当做普通函数来执行,并通过call
指定this
,从而实现实例自身属性的继承,然后再通过Object.create
指定子构造函数的原型对象。
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; // 继承原型方法 // 创建一个新对象,使用一个已经存在的对象作为新创建对象的原型 B.prototype = Object.create(A.prototype); B.prototype.constructor = B; function B () { // 继承私有方法 A.call(this); // 如果有参数的话可以在这里传入 this.b = 200; } B.prototype.getB = function () { console.log(this.b); };
这里我们再次通过画图的形式梳理一下逻辑:
下面我们创建B
的实例,看下是否成功继承了A
中的属性和方法。
const b = new B(); console.log('b', b.a); b.getA(); console.log('b', b.b); b.getB(); // b 100 // 100 // b 200 // 200
class extends实现继承
在es6
中为开发者提供了extends
关键字,可以很方便的实现类之间的继承:
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; // 继承原型方法 // 创建一个新对象,使用一个已经存在的对象作为新创建对象的原型 B.prototype = Object.create(A.prototype); B.prototype.constructor = B; function B () { // 继承私有方法 A.call(this); // 如果有参数的话可以在这里传入 this.b = 200; } B.prototype.getB = function () { console.log(this.b); };
下面我们创建B
的实例,看下是否成功继承了A
中的属性和方法。
const b = new B(); console.log('b', b.a); b.getA(); console.log('b', b.b); b.getB(); // b 100 // 100 // b 200 // 200
大家可能会好奇class
的extends
关键字是如何实现继承的呢?下面我们用babel
编译代码,看下其源码中比较重要的几个点:
看下这俩个方法的实现:
值得留意的一个地方是:extends
将父类的静态方法也继承到了子类中
class A { constructor () { this.a = 100; } getA () { console.log(this.a); } } A.say = function () { console.log('say'); }; class B extends A { constructor () { // 继承私有方法 super(); this.b = 200; } getB () { console.log(this.b); } } B.say(); // say
extends的实现类似于方案二:
apply
方法更改父类this
指向,继承私有属性Object.create
继承原型属性Object.setPrototypeOf
继承静态属性
结语
理解JavaScript
的原型原型链可能并不会直接提升你的JavaScrit
编程能力,但是它可以帮助我们更好的理解JavaScript
中一些知识点,想明白一些之前不太理解的东西。在各个流行库或者框架中也有对于原型或原型链的相关应用,学习这些知识也可以为我们阅读框架源码奠定一些基础。
加载全部内容