前言
在js的学习中,原型毫无疑问是一个难点,但也是一个不可忽视的重点。在前端面试中也是一个高频考题,在接下来的深入学习中,你会发现原型、原型链等知识点其实并不难。
1. “一切皆为对象”
JavaScript是一个面向(原型)对象的语言,对象是属性的集合,除了值类型 ”一切(引用类型)皆为对象“,判断一个变量是不是对象,值类型可以用typeof,引用类型用instanceof。
我们可以通过new来创建一个对象,但其实所有对象都是通过函数创建的,而函数也是对象。等等,这好像有点绕,不急,让我们先去了解prototype原型。
2. 原型
prototype
在JavaScript中,所有的函数默认都会拥有一个名为prototype的公有且不可枚举的属性,它会指向另一个对象:这个对象通常被称为函数的原型。简单来说,prototype就是函数的一个属性,这个prototype的属性值是一个对象(对象是属性的集合)。
function Foo() {}
var a = new Foo;
console.log(a.__proto__ === Foo.prototype); // true
上面代码通过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
前面我们提到过所有对象都是通过函数创建的,而函数也是一种对象,那函数是谁创建的呢?答案是 Function :
function Foo(a){return a;
};
console.log(Foo(7)); // 7var Foo1 = new Function("a","return a");
console.log(Foo1(6)); // 6console.log(Foo.__proto__ === Function.prototype); // true
我们可以通过new Function来创建一个函数,它跟我们平时创建的函数达到了一样的效果,这种方式不推荐去使用,现在我们还可以得出Foo.__proto__ === Function.prototype
。
除了Function,我们还要介绍一下 Object:
var obj = {};
console.log(obj.__proto__ === Object.prototype); // true
跟函数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
。
说到这里你可能有点晕了,我们可以先放一放,接下去看一下原型和隐式原型之间关联在一起的意义。
3. 原型链
我们先来看一段代码:
function Foo() {}
Foo.prototype.a = 7;
Foo.prototype.sayHi = function(){console.log('Hello');
}
var bar = new Foo();
console.log(bar.a); // 7
bar.sayHi(); // Hello
访问对象属性时,引擎实际上会调用内部的Get操作,这个操作会检查对象本身是否包含这个属性,如果没找到还会继续沿着__proto__
向上查找直到尽头,这就是 原型链 ,所有普通的原型链最终都会指向内置的Object.prototype,它为null。 所以在上面代码,能从原型链上找到a属性。
借助原型链,Function函数的prototype中的一些方法和属性(call,apply,bindarguments…)可以在每一个函数中使用,因为函数由Function函数创建。对象也是如此,Object.prototype上的(toString, valueOf, isPrototypeOf…)也可以方便我们使用。 我们也可以在prototype中添加自定义属性。
继承?
创建的新对象可以使用另一个对象上的属性和函数, 原型链的机制很容易让我们联想到其他语言中的继承。继承意味着复制操作,但JavaScript并不会复制对象属性,JavaScript会在两个对象之间创建关联(prototype),意味着某些对象在找不到属性和方法引用时会把这个请求 委托 给另一个对象。 委托这个词能更准确地描述JavaScript中对象的关联机制。
4. ”构造函数“
function Foo() {}
console.log(Foo.prototype.constructor === Foo); // true
var bar = new Foo();
console.log(bar.constructor === Foo); // true
每一个函数原型上(本例中Foo.prototype)默认有一个公有并且不可枚举的属性constructor,这个属性引用的是对象关联的函数(Foo),在上面的代码中,我们会把Foo这个函数看做是一个”构造函数“,因为我们看到它“构造”了这个对象。我们可能会习惯用constructor来去理解判断。
那能否通过bar.constructor === Foo
的结果来判断bar的”构造函数“是不是Foo呢。
function Foo() {}
Foo.prototype = {}; // 新的原型对象
console.log(Foo.prototype.constructor === Foo); // falsevar bar = new Foo();
console.log(bar.constructor === Foo); // false
console.log(bar.constructor === Object); // true
通过上面的代码我们可以发现,我们看起来应该是Foo函数”构造“了bar,但事实上这里的bar.constructor并没有指向Foo,反而是指向了Object函数。
原因:
我们懂了原型链就会知道这并不奇怪,bar其实并没有constructor属性,bar会委托原型链上的Foo.prototype。 由于我们给Foo创建了一个新原型对象,所以Foo已经没有了默认的constructor属性,然后a会继续沿着原型链查找直到原型链的顶端Object.prototype,这个对象上有constructor属性(每一个函数原型上都默认有)。
所以bar.constructor并不是一个安全可靠的引用,有时会指向你意想不到的地方。
实际上Foo和其他函数没有什么区别,函数本身并不是构造函数,在JavaScript中对于”构造函数“最准确的解释是:所有带new的函数调用。
5. Object.create 和 new
在前文我们多次使用到了new,可以看出使用new发生构造函数调用时,会创建一个新对象,这个新对象会被执行原型连接。直接点解释就是这个新对象的隐式原型链接到”构造函数“的原型对象。
二者有什么联系:
Object.create( )是Object的内置方法,它也会创建一个新对象,跟new有什么区别呢?我们先来看Object.create( )的polyfill代码,它部分实现了Object.create( )。
if (!Object.create) {Object.create = function(o) {function F(){}F.prototype = o;return new F();};
}
可以看到,这个代码使用一个一次性函数F,通过改写它的prototype属性使其指向想要关联的对象,我们使用下面这段代码来说明:执行这一行Object.create( )时加入polyfill代码,可以推出(var b = new F( ) ,F.prototype = foo),相当于b这个新对象的内部原型链接到foo。
var foo = {sayHi: function() {console.log("hello")}
};
var bar = Object.create(foo);
bar.sayHi(); // hello
Object.create(),该新对象(b)的隐式原型指向现有对象(foo),这样不仅可以充分利用原型还避免了一些不必要的麻烦(比如constructor)。
6. 内省
通过内省找出对象的”祖先“(委托关联)
instanceof
a instanceof Foo:会判断在a的整条__proto__
链中是否有Foo.prototype指向的对象,即之中是否有同一个对象。
isPrototype()
b.isPrototype©:会判断b是否出现在c的prototype链中。
function Foo() {}
// a的隐式原型 链接到Foo.prototype所指的对象
var a = new Foo();
console.log(a instanceof Foo); // true
console.log(Foo.prototype.isPrototypeOf(a)); // true// Bar的隐式原型 链接到Foo
var Bar = Object.create(Foo);
console.log(Foo.isPrototypeOf(Bar)); // true // a.prototype这个对象的隐式原型 链接到Foo.prototype
a.prototype = Object.create(Foo.prototype);
console.log(a.prototype instanceof Foo); // true
console.log(Foo.prototype.isPrototypeOf(a.prototype)); // true
结语
我们可以从下图中看到十分复杂的关系图,但需要我们耐心去看去分析,可以从中收获很多。