JS原型、原型链到底是什么?

前言

在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.prototypea.__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)); // 7

var Foo1 = new Function("a","return a");
console.log(Foo1(6)); // 6

console.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 创建的。

注意,前面我们说的 FunctionObject 都是函数,也是对象。也就是说, 函数由 Function 函数创建,而函数又是对象,对象由 Object 函数创建,然后函数又由 Function 函数创建......Foo.__proto__ === Function.prototype -->Object.__proto__ === Function.prototype-->Function.__proto__ === Function.prototype?怎么感觉进入了循环?我造我自己?用一个图来表示:

微信图片_20220502170639.jpg

由上图可以发现,这其实就是一个无限循环!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); // false

var 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(c):会判断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


结语

我们可以从下图中看到十分复杂的关系图,但需要我们耐心去看去分析,可以从中收获很多。

微信图片_20220502184449.png

参考资料

猜你喜欢

转载自juejin.im/post/7093376466754699277