理解原型、原型链
前言
在js的学习中,原型毫无疑问是一个难点,但也是一个不可忽视的重点。在前端面试中也是一个高频考题,在接下来的深入学习中,你会发现原型、原型链等知识点其实并不难。
一切皆为对象
JavaScript是一个面向(原型)对象的语言,对象是属性的集合,除了值类型 ”一切(引用类型)皆为对象“,判断一个变量是不是对象,值类型可以用typeof,引用类型用instanceof。
我们可以通过new来创建一个对象,但其实所有对象都是通过函数创建的,而函数也是对象。等等,这好像有点绕,不急,让我们先去了解prototype原型。
原型
prototype
在JavaScript中,所有的函数默认都会拥有一个名为prototype的公有且不可枚举的属性,它会指向另一个对象:这个对象通常被称为函数的原型。简单来说,prototype就是函数的一个属性,这个prototype的属性值是一个对象(对象是属性的集合)。
1 | function Foo() {} |
上面代码通过new Foo()创建a时,将a内部的隐式原型( __proto__ )链接到Foo.prototype所指的对象,即 a.__proto__ === Foo.prototype,a.__proto__ 指向了 Foo.prototype
隐式原型__proto__
我们现在知道每个 函数 都有一个prototype,那再介绍一个隐式原型, __proto__是每个 对象 都有的一个隐式原型,换句话说,这个奇怪的__proto__引用了内部的原型对象,实际上__proto__并不存在你正在使用的对象中,当获取a.__proto__时,实际上调用了a.__proto__(调用了getter函数),getter不在我们目前的讨论范围内了,我们只需要知道__proto__是我们用来获取对象内部原型的方法,跟es5的标准Object.getPrototypeOf(a)结果一样。
让我们梳理一下,我们可以通过new Foo()创建a,使a.__proto__指向创建该对象的函数的prototype 。
函数也是对象,函数也有__proto__吗
前面我们提到过所有对象都是通过函数创建的,而函数也是一种对象,那函数是谁创建的呢?答案是 Function :
1 | function Foo(a){ |
我们可以通过new Function来创建一个函数,它跟我们平时创建的函数达到了一样的效果,这种方式不推荐去使用,现在我们还可以得出Foo.__proto__ === Function.prototype。
除了Function,我们还要介绍一下 Object:
1 | var obj = {}; |
跟函数Foo是被Function创建的一样,obj本质上也是由 Object 创建的。
注意,前面我们说的 Function 和 Object 都是函数,也是对象。也就是说, 函数由 Function 函数创建,而函数又是对象,对象由 Object 函数创建,然后函数又由 Function 函数创建……Foo.__proto__ === Function.prototype –> Object.__proto__ === Function.prototype –> Function.__proto__ === Function.prototype?怎么感觉进入了循环?我造我自己?用一个图来表示:

由上图可以发现,这其实就是一个无限循环!Function 是一个函数,函数又是一种对象,也有 __proto__ 属性。Function 这个函数就是被自身所创建,它的 __proto__ 指向了自身的prototype。
说到这里你可能有点晕了,我们可以先放一放,接下去看一下原型和隐式原型之间关联在一起的意义。
原型链
我们先来看一段代码:
1 | function Foo() {} |
访问对象属性时,引擎实际上会调用内部的Get操作,这个操作会检查对象本身是否包含这个属性,如果没找到还会继续沿着 __proto__ 向上查找直到尽头,这就是 原型链 ,所有普通的原型链最终都会指向内置的Object.prototype,它为null。 所以在上面代码,能从原型链上找到a属性。
借助原型链,Function函数的prototype中的一些方法和属性(call,apply,bindarguments…..)可以在每一个函数中使用,因为函数由Function函数创建。对象也是如此,Object.prototype上的(toString, valueOf, isPrototypeOf……)也可以方便我们使用。
我们也可以在prototype中添加自定义属性。
继承
创建的新对象可以使用另一个对象上的属性和函数, 原型链的机制很容易让我们联想到其他语言中的继承。继承意味着复制操作,但JavaScript并不会复制对象属性,JavaScript会在两个对象之间创建关联(prototype),意味着某些对象在找不到属性和方法引用时会把这个请求 委托 给另一个对象。
委托这个词能更准确地描述JavaScript中对象的关联机制。
构造函数
1 | function Foo() {} |
每一个函数原型上(本例中Foo.prototype)默认有一个公有并且不可枚举的属性constructor,这个属性引用的是对象关联的函数(Foo),在上面的代码中,我们会把Foo这个函数看做是一个”构造函数“,因为我们看到它“构造”了这个对象。我们可能会习惯用constructor来去理解判断。
那能否通过 bar.constructor === Foo 的结果来判断bar的”构造函数“是不是Foo呢。
1 | function Foo() {} |
通过上面的代码我们可以发现,我们看起来应该是Foo函数”构造“了bar,但事实上这里的bar.constructor并没有指向Foo,反而是指向了Object函数。
原因:
我们懂了原型链就会知道这并不奇怪,bar其实并没有constructor属性,bar会委托原型链上的Foo.prototype。
由于我们给Foo创建了一个新原型对象,所以Foo已经没有了默认的constructor属性,然后a会继续沿着原型链查找直到原型链的顶端Object.prototype,这个对象上有constructor属性(每一个函数原型上都默认有)。
所以bar.constructor并不是一个安全可靠的引用,有时会指向你意想不到的地方。
实际上Foo和其他函数没有什么区别,函数本身并不是构造函数,在JavaScript中对于”构造函数“最准确的解释是:所有带new的函数调用。
Object.create 和 new
在前文我们多次使用到了new,可以看出使用new发生构造函数调用时,会创建一个新对象,这个新对象会被执行原型连接。直接点解释就是这个新对象的隐式原型链接到”构造函数“的原型对象。
二者有什么联系
Object.create()是Object的内置方法,它也会创建一个新对象,跟new有什么区别呢?我们先来看Object.create()的polyfill代码,它部分实现了Object.create()。
1 | if (!Object.create) { |
可以看到,这个代码使用一个一次性函数F,通过改写它的prototype属性使其指向想要关联的对象,我们使用下面这段代码来说明:执行这一行Object.create()时加入polyfill代码,可以推出(var b = new F( ) ,F.prototype = foo),相当于b这个新对象的内部原型链接到foo。
1 | var foo = { |
Object.create(),该新对象(b)的隐式原型指向现有对象(foo),这样不仅可以充分利用原型还避免了一些不必要的麻烦(比如constructor)。
内省
通过内省找出对象的”祖先“(委托关联)
instanceof
a instanceof Foo:会判断在a的整条 __proto__ 链中是否有Foo.prototype指向的对象,即之中是否有同一个对象。
isPrototype
b.isPrototype(c):会判断b是否出现在c的prototype链中。
1 | function Foo() {} |
总结
我们可以从下图中看到十分复杂的关系图,但需要我们耐心去看去分析,可以从中收获很多。

最后
本文转载自稀土掘金 https://juejin.cn/post/7093376466754699277,作者为 离文不问

