Skip to content

原型链

前言

我们都知道js语言有原型链的概念,每个对象都会有个__proto__属性,它要么是null,要么就是对另一个对象的引用,称之为原型。先看几个小例子简单了解一下什么是原型。

tip:刚接触原型链的概念时,会分不清__proto__[[Prototype]]的区别。其实规范里不使用__proto__属性来保存原型,而是使用隐藏的属性[[Prototype]]。不过我们使用__proto__也没有问题。因为在规范没出来前,所有的浏览器包括服务端都已经支持了__proto__的写法,所以该属性就一直被保留了下来。

它俩还是有区别的,__proto__ 实际是 [[Prototype]]getter/setter__proto__ 属性目前来看有点过时了,现代编程语言建议使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型,但是由于 __proto__ 标记在观感上更加明显,所以我们在后面的示例中将使用它。

原型其实就是个对象

先来看一个小例子理解下原型的概念。

js
let animal = {
    eats: true
};
let rabbit = {
    jumps: true,
    __proto__: animal // 设置 rabbit.[[Prototype]] = animal
};
console.log(rabbit.jumps) // true
console.log(rabbit.eats) // true

如上面的例子,animal对象就是rabbit对象的原型。在调用rabbit.eats时,因为rabbit对象自身并没有这个属性,所以就会顺着原型向上找,在animal对象中找到了eats属性,所以输出了true

从上面的例子看出,原型其实就是一个对象。

原型链可以很长

原型链可以很长,比如下面这个例子

js
let animal = {
    eats: true,
    walk() {
        alert("Animal walk");
    }
};

let rabbit = {
    jumps: true,
    __proto__: animal
};

let longEar = {
    earLength: 10,
    __proto__: rabbit
};

// walk 是通过原型链获得的
longEar.walk(); // Animal walk
console.log(longEar.jumps); // true(从 rabbit)

现在,如果从 longEar 中读取一些它不存在的内容,JavaScript 会顺着原型链一层一层往上查找(先找 rabbit ,然后 animal )。

原型链的两点注意事项

这里会有两个限制:

  1. 引用不能形成闭环。如果我们试图在一个闭环中分配 __proto__,JavaScript 会抛出错误。

    js
    let animal = {
        eats: true,
        walk() {
            alert("Animal walk");
        },
        __proto__: animal
    };
    /* 
    	ReferenceError: Cannot access 'animal' before initialization
    */
  2. __proto__ 的值可以是对象,也可以是 null。而其他的类型都会被忽略。

    忽略的意思是还按之前的引用。比如,

    let arr1 = new Array(),此时arr1__proto__默认指向Array.prototype

    let o1 = {},此时o1__proto__默认指向Object.prototype

    js
    let animal = {
        eats: true,
        walk() {
            alert("Animal walk");
        }
    };
    
    let arr1 = new Array()
    console.log(arr1.__proto__ === Array.prototype); // true
    arr1.__proto__ = 'xxxx' // 赋值的不是对象,也不是null,会被忽略
    console.log(arr1.__proto__ === Array.prototype); // true
    arr1.__proto__ = null // 赋值为null
    console.log(arr1.__proto__); // undefined,这里很有意思,赋值为null,但是输出的是 undefined
    arr1.__proto__ = animal // 赋值为对象
    console.log(arr1.__proto__); // { eats: true, walk: [Function: walk] }

__proto__的最顶部是null

原型链可以有很长很长,那它的最顶部是个啥呢?我们来看这样一个图,看懂这个图,相信会对原型链有个很清晰的认知。(图片来自:https://github.com/KieSun/Dream/issues/2

image-20211109163134065

先来弄懂下面几个问题

图中有多少个对象?

相信很多人会直接回答,有四个!f2、f1、o1、o2,只有它们四个使用typeof时得到的值是object,所以就只有它们四个是对象。

其实是不正确的,即便typeof得到的值是function,它也是一个对象,称之为函数对象(也可以叫函数、也可以叫方法,或者叫构造方法)。图中的ObjectFunction就是函数对象,它们是JS的内置对象。

tip:JS中对象分为两种,函数对象一般对象。我们使用function xx(){}声明出来的都是函数对象。

声明一般对象时我们可以使用new关键字(new声明出来的对象,我们也叫实例对象,只有构造方法才能使用new关键字来声明实例对象),比如let o1 = new Object()或者let a1 = new Array()

或者直接用let a = {},其实这种方式等效于let a = new Object()

JS有很多内置对象,除了图中的ObjectFunction,还有很多,比如Boolean、Symbol、Error、Math、JSON、Date等等。这些内置对象大部分是函数对象,还有小部分是一般对象。函数对象使用typeof得到的值为function,一般对象得到的值为object。(null是个例外,typeof得到的值也是object,这是个历史遗留问题,特殊记一下)

js
function Foo() { }
let f1 = new Foo()
let f2 = new Foo()
let o1 = new Object()
let o2 = new Object()

console.log(typeof f1); // object
console.log(typeof f2); // object
console.log(typeof o1); // object
console.log(typeof o2); // object

console.log(typeof Foo); // function
/* 基本对象 */
console.log(typeof Object); // function
console.log(typeof Function); // function

/* 错误对象 */
console.log(typeof Error); // function

/* 数字和日期对象 */
console.log(typeof Number); // function
console.log(typeof Date); // function
console.log(typeof Math, 'Math 不是一个函数对象,因此它是不可构造的'); // object 

/* 字符串对象 */
console.log(typeof String); // function
console.log(typeof RegExp); // function

/* 可索引的集合对象 */
console.log(typeof Array); // function

/* 使用键的集合对象 */
console.log(typeof Map); // function
console.log(typeof Set); // function

/* 结构化数据 */
console.log(typeof ArrayBuffer); // function
console.log(typeof JSON, 'JSON 不是一个函数对象,因此它是不可构造的'); // object

/* 控制抽象对象 */
console.log(typeof Promise); // function

/* 反射 */
console.log(typeof Reflect, 'Reflect不是一个函数对象,因此它是不可构造的。'); // object
console.log(typeof Proxy); // function

console.log(typeof null, 'typeof null的值是object,是个历史遗留问题,特殊记一下');

xxx.prototype是个啥?

知道了什么是函数对象一般对象后,我们继续看图,又有了个疑问。图中所有对象的__proto__(原型)都指向了xxx.prorotype上面,那么这个xxx.prorotype又是个啥?

实际上每个函数对象,默认都会有个prototype属性prototype实际就是一个对象,上面可以挂载很多方法和属性),我们一直说的原型其实就是它。所有对象的__proto__都会指向原型上,一层一层的往上指,一直指到null。每当我们在对象上找一个属性或方法时,如果自己没有,就会去原型链上找,一层一层的找,直到找到为止。

tip:函数对象的prototype是我们声明对象时,js默认添加上的。比如

let o1 = {},对象o1__proto__默认会指向Object.prototype,即:o1.__proto__ === Object.prototype

let o2 = new Array(),对象o2__proto__默认会指向Array.prototype,即:o2.__proto__ === Array.prototype

分析f1f2

我们先来分析一下图中的f1f2两个对象:

js
function Foo() { }
let f1 = new Foo()
let f2 = new Foo()

console.log(f1.__proto__ === Foo.prototype); // true
console.log(f2.__proto__ === Foo.prototype); // true

console.log(f1.__proto__.__proto__ === Object.prototype); // true
console.log(f1.__proto__.__proto__.__proto__ === null); // true

通过打印输出,验证了我们之前的分析是正确的。f1f2两个对象的__proto__都指向Foo.prototype,即f1.__proto__ === Foo.prototype。(f2同理)

而又因为Foo.prototype属性本身就是一个对象,对象都会有属于它的原型(有__proto__),所有对象的原型默认都指向Object.prototype,所以Foo.prototype.__proto__ === Object.prototype,即f1.__proto__.__proto__ === Object.prototype

那么问题来了,如果说所有的原型都是一个对象,对象的原型默认都指向Object.prototype,那Object.prototype按理说也应该是个对象,那它不就自己指自己了,这种情况不就报错了?那 Object.prototype的原型指向哪里呢?

其实我们标题已经给出了答案,没错,是null,我们已经找到头了,Object.prototype.__proto__ === null,即f1.__proto__.__proto__.__proto__ === null

分析o1o2

我们再来分析一下图中的o1o2两个对象:o1o2的分析和f1f2一样,都是类似的,直接看运行结果。

js
let o1 = new Object()
let o2 = new Object()
let o3 = {} // 可以看成 let o3 = new Object()

console.log(o1.__proto__ === Object.prototype); // true
console.log(o2.__proto__ === Object.prototype); // true
console.log(o3.__proto__ === Object.prototype); // true

console.log(o1.__proto__.__proto__ === null); // true

分析Foo

根据上文,我们已经知道,对象分为一般对象和函数对象,一般对象原型__proto__默认都指向Object.prototype,那么函数对象的原型默认指向哪呢?答案是,指向Function.prototype

函数对象(也成为构造函数或函数)是使用function关键字声明出来的,图中的Foo就是一个函数对象。

js
function Foo() { }

console.log(Foo.__proto__ === Function.prototype); // true
console.log(Foo.__proto__.__proto__ === Object.prototype); // true
console.log(Foo.__proto__.__proto__.__proto__ === null); // true

通过打印输出,验证了我们的分析是正确的。Foo的原型指向了Function.prototype,即:Foo.__proto__ === Function.prototype

Function.prototype本身就是一个对象,所以它的原型默认指向Object.prototype,即:Foo.__proto__.__proto__ === Object.prototype

Object.prototype的原型指向null,即:Foo.__proto__.__proto__.__proto__ === null

分析Function

我们已经知道函数对象的原型默认都指向Function.prototype,那么问题来了Function本身就是一个函数对象,那它的原型指向哪呢?没有错,指向它自己的prototype,即Function.__proto__ === Function.prototype,图上也是这么画的,我们举例来验证一下。

js
console.log(Function.__proto__ === Function.prototype); // true
console.log(Function.__proto__.__proto__ === Object.prototype); // true
console.log(Function.__proto__.__proto__.__proto__ === null); // true

通过打印输出,验证了我们的分析是正确的。Function的原型指向了Function.prototype,即:Function.__proto__ === Function.prototype

Function.prototype本身就是一个对象,所以它的原型默认指向Object.prototype,即:Function.__proto__.__proto__ === Object.prototype

Object.prototype的原型指向null,即:Function.__proto__.__proto__.__proto__ === null

分析Object

根据上面的一大堆分析,到现在我们已经能猜测出结果了。因为Object也是一个内置的函数对象,所以它的原型应该指向Function.prototype,然后剩下的就都一样了。图上也是这么画的,我们拿例子验证一下。

js
console.log(Object.__proto__ === Function.prototype); // true
console.log(Object.__proto__.__proto__ === Object.prototype); // true
console.log(Object.__proto__.__proto__.__proto__ === null); // true

根据打印输出,验证我们的猜想是正确的。Object的原型指向了Function.prototype,即:Object.__proto__ === Function.prototype

Function.prototype本身就是一个对象,所以它的原型默认指向Object.prototype,即:Object.__proto__.__proto__ === Object.prototype

Object.prototype的原型指向null,即:Object.__proto__.__proto__.__proto__ === null

以上,已经分析完了所有情况,其实就是这么简单。

原型实际就是一个对象,function声明出来的函数对象(也称构造函数或函数),它的原型默认指向Function.prototype。JS内置的函数对象的原型默认也指向Function.prototype。顺着原型链往上找,最终肯定能指到Object.prototype,再往上就是null了。

constructor是个啥?

整张图我们已经分析的差不多了,但是图中还遗漏了一处,constructor是个啥?

看图我们发现,Foo.prototype.constructor又指回了FooObject.prototype.constructor又指回了ObjectFunction.prototype.constructor又指回了Function。我们自己来验证一下:

js
function Foo() { }
let f1 = new Foo()

console.log(Foo.prototype.constructor === Foo); // true
console.log(Object.prototype.constructor === Object); // true
console.log(Function.prototype.constructor === Function); // true

console.log(f1.constructor === Foo); // true

通过验证我们发现,确实是这么回事,原型的constructor属性又指向了函数对象(构造函数)本身。不仅是原型保存了这个函数对象(构造函数),实例对象也保存了一份,如示例中的f1.constructor === Foo。那constructor的作用是干什么的呢?

作用一:类型判断

其实,constructor就是保存了一份对象的构造方法,好让我们知道这个对象是从哪里来的。要说它真正的作用,JS底层可能会用到,以下是我的猜测:

底层用于类型判断 比如typeof

我们在使用function关键字声明一个函数时,其实这个函数就是一个构造函数,本文中我们也称之为函数对象,工作中我们习惯的叫法说它是个函数。其实不管怎么称呼,只要是function声明出来的,我们都可以使用new关键字来构建一个实例对象,然后实例对象就可以通过原型链,访问原型上面的方法。

所以,当使用function声明一个构造方法时,JS为它自动绑定了一个prototype对象,然后又自动的往prototype塞了一个自身的引用constructor,即xxx.prototype.constructor === xxx。只要是这种格式的对象,JS都认定它是函数对象(构造方法),可以使用new关键字来声明实例对象。使用typeof进行类型校验时,会返回function

假如我们随便定义一个非function类型(假如是个number),我们强行对齐使用new,就会报错。比如:

let a = 1,对a强行使用newlet o1 = new a,JS会抛出一个错误,TypeError: a is not a constructor,所以我猜想保存constructor除了告诉我们该对象是从哪里来的,还一个作用就是为了用作类型判断,所有的构造函数typeof之后的值都为function

补充个MDN给构造函数的定义:

构造函数属于被实例化的特定类对象 。构造函数初始化这个对象,并提供可以访问其私有信息的方法。构造函数的概念可以应用于大多数面向对象的编程语言。本质上,JavaScript 中的构造函数通常在的实例中声明。(跟自己理解的差不多,构造函数本身也是一个对象。)

参考:https://developer.mozilla.org/zh-CN/docs/Glossary/Constructor

作用二:创建新的实例对象

除了上述作用,我们可以使用 constructor 属性来创建一个新对象,该对象使用与现有对象相同的构造器。比如:

js
function Rabbit(name) {
  this.name = name;
  console.log(name);
}

let rabbit = new Rabbit("White Rabbit");

let rabbit2 = new rabbit.constructor("Black Rabbit");

当我们有一个对象,但不知道它使用了哪个构造器(例如它来自第三方库),并且我们需要创建另一个类似的对象时,用这种方法就很方便。

使用constructor的注意事项

……JavaScript 自身并不能确保正确的 "constructor" 函数值。

它存在于函数的默认 prototype 中,但仅此而已。之后会发生什么 —— 完全取决于我们。

如果我们将整个默认 prototype 替换掉,那么其中就不会有 constructor 了,比如:

js
function Rabbit() {}
Rabbit.prototype = {
  jumps: true
};

let rabbit = new Rabbit();
console.log(rabbit.constructor === Rabbit); // false

因此,为了确保正确的 constructor,我们可以选择添加/删除属性到默认的 prototype中,而不是将其整个覆盖掉:

js
function Rabbit() {}

// 不要将 Rabbit.prototype 整个覆盖
// 可以向其中添加内容
Rabbit.prototype.jumps = true
// 默认的 Rabbit.prototype.constructor 被保留了下来

或者,也可以手动重新创建 constructor 属性:

js
Rabbit.prototype = {
  jumps: true,
  constructor: Rabbit
};

// 这样的 constructor 也是正确的,因为我们手动添加了它

总结

  • 在 JavaScript 中,所有的对象都有一个隐藏的 [[Prototype]] 属性,它要么是另一个对象,要么就是 null
  • 通过 [[Prototype]] 引用的对象被称为“原型”。
  • __proto__属于[[prototype]]getter/setter__proto__ 并不是语言本身的特性,是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法或属性。
  • 如果我们想要读取 obj 的一个属性或者调用一个方法,并且它不存在,那么 JavaScript 就会尝试在原型中查找它。
  • 原型的最顶部是null
  • 通过构造方法(假设为Foonew出来的对象(假设为o),会默认有个constructor属性,该属性指向它的构造方法,即o.constructor ===Foo
  • 通过构造方法(假设为Foonew出来的对象(假设为o),它的__proto__会默认指向它的构造方法的原型上,即o.__proto__ ===Foo.prototype
  • 构造方法(假设为Foo)的原型上默认有个constructor属性,该属性执行它自己,即Foo.prototype.constructor === Foo

参考

https://github.com/KieSun/Dream/issues/2

https://zh.javascript.info/prototype-inheritance

https://zh.javascript.info/function-prototype

https://es6.ruanyifeng.com/#docs/class