一文搞懂JavaScript的原型链(轻松实现结构化理解)

279 阅读5分钟

前言

大家好,我是抹茶。
原型链一直都是JavaScript中晦涩难懂的部分,希望这篇文章能帮助你形成自己的理解。

术语关注

要弄懂原型链,我们要关注下面四个属性

  • [[Prototype]]
  • prototype
  • constructor
  • __proto__

怎么,发现有[[Prototype]]prototype这两个东西?规范一般以[[]]表示的是内部属性。

[[Prototype]]用于在对象身上,是指向对象的原型对象的指针。

prototype是用在函数对象上,也是指向函数对象的原型对象的指针。

这些概念是不是每个字都认识,但是每个都不好理解?后面我们会看具体🌰。

JS内部的原生函数

在JavaScript内部有一些原生函数

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()

多数情况下,对象的内部的[[Class]]属性和创建该对象的内建原生函数相对应。

const arr = [123];
Object.prototype.toString.call(arr);// '[object Array]'

const num = 123;
Object.prototype.toString.call(123);// '[object Number]'

const str = '字符串';
Object.prototype.toString.call(str);// '[object String]'

JS引擎会自动给基本类型值包装封装对象

JavaScript中的基本类型值会被各自的封装对象(可以理解为内置函数)自动包装, 当我们写str.length的时候,其实基本类型是没有.length.toString()这样的属性和方法,需要通过封装对象才能访问,此时JavaScript引擎会自动为基本类型值包装一个封装对象。

这个封装对象就是内置函数,也就是这个变量的原型。

JS内置默认的原型链

1.字符串类型的原型链

// str => String.prototype => Object.prototype => null
const str = '字符串';
const str1 = new String('字符串');
Object.prototype.toString.call(str);// '[object String]'
console.log(Object.getPrototypeOf(str));//{}
console.log(Object.getPrototypeOf(str) === String.prototype);//true
console.log(Object.getPrototypeOf(str1) === String.prototype);//true
console.log(Object.getPrototypeOf(String.prototype) === Object.prototype);//true
console.log(Object.getPrototypeOf(Object.prototype) === null);// true

可以推测出str变量会在JS引擎内部创建一个new String('字符串')的封装对象,这个对象的原型是String.prototypeString.prototype的原型是Object.prototype

对比str 和 str1 两种变量声明的方式,可以看到new 构造调用返回的是一个对象,而[[Prototype]]是对象上的内置属性。

image.png

上面也提到String()是内置函数,而函数本身也可以有原型链,所以通过.prototype的方式为函数指向原型,用[[Prototype]]为对象指向原型,也算是一个小小的设计区分点。

我们再来看其他类型。

2.数字类型的原型链

// num => Number.prototype => Object.prototype => null
const num = 123;
const num1 = new Number(123);
console.log(Object.prototype.toString.call(num));// '[object Number]'
console.log(Object.getPrototypeOf(num));//{}
console.log(Object.getPrototypeOf(num1));//{}
console.log(Object.getPrototypeOf(num) === Number.prototype);//true
console.log(Object.getPrototypeOf(num1) === Number.prototype);//true
console.log(Object.getPrototypeOf(Number.prototype) === Object.prototype);//true
console.log(Object.getPrototypeOf(Object.prototype) === null);// true

3.布尔类型的原型链

// bol => Boolean.prototype => Object.prototype => null
const bol = true;
const bol1 = new Boolean(true);
console.log(Object.prototype.toString.call(bol));// '[object Boolean]'
console.log(Object.getPrototypeOf(bol));//{}
console.log(Object.getPrototypeOf(bol1));//{}
console.log(Object.getPrototypeOf(bol) === Boolean.prototype);//true
console.log(Object.getPrototypeOf(bol1) === Boolean.prototype);//true
console.log(Object.getPrototypeOf(Boolean.prototype) === Object.prototype);//true
console.log(Object.getPrototypeOf(Object.prototype) === null);// true

4.Array类型的原型链

// arr => Array.prototype => Object.prototype => null
const arr = [123];
const arr1 = new Array(123);
console.log(Object.prototype.toString.call(arr));// '[object Array]'
console.log(Object.getPrototypeOf(arr));// Object(0) []
console.log(Object.getPrototypeOf(arr1));// Object(0) []
console.log(Object.getPrototypeOf(arr) === Array.prototype);//true
console.log(Object.getPrototypeOf(arr1) === Array.prototype);//true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype);//true
console.log(Object.getPrototypeOf(Object.prototype) === null);// true

5.Date类型的原型链

// date => Date.prototype => Object.prototype => null
const date = new Date();
console.log(Object.prototype.toString.call(date));// '[object Date]'
console.log(Object.getPrototypeOf(date));// {}
console.log(Object.getPrototypeOf(date) === Date.prototype);// true
console.log(Object.getPrototypeOf(Date.prototype) === Object.prototype);// true
console.log(Object.getPrototypeOf(Object.prototype) === null);// true

6.正则类型的原型链

// patt => RegExp.prototype => Object.prototype => null
const patt = new RegExp("e");
console.log(Object.prototype.toString.call(patt));// '[object RegExp]'
console.log(Object.getPrototypeOf(patt));// {}
console.log(Object.getPrototypeOf(patt) === RegExp.prototype);// true
console.log(Object.getPrototypeOf(RegExp.prototype) === Object.prototype);// true
console.log(Object.getPrototypeOf(Object.prototype) === null);// true

7.Function 类型的原型链

// fn => Function.prototype => Object.prototype => null
const fn = new Function("a",'b', 'return a + b');
console.log(Object.prototype.toString.call(fn));// '[object Function]'
console.log(Object.getPrototypeOf(fn));// {}
console.log(Object.getPrototypeOf(fn) === Function.prototype);// true
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype);// true
console.log(Object.getPrototypeOf(Object.prototype) === null);// true

8.Error 类型的原型链

// err => Error.prototype => Object.prototype => null
const err = new Error('报错');
console.log(Object.prototype.toString.call(err));// '[object Error]'
console.log(Object.getPrototypeOf(err));// {}
console.log(Object.getPrototypeOf(err) === Error.prototype);// true
console.log(Object.getPrototypeOf(Error.prototype) === Object.prototype);// true
console.log(Object.getPrototypeOf(Object.prototype) === null);// true

根据上面的例子我们可以看出,原型默认情况下就是变量的封装对象,对应于某一个内置函数。函数本身也会有prototype,指向他关联到了另一个对象。这里不包括null和undefined,因为我们不会对这两个类型做什么操作,他们也没有对应的封装对象(内置函数),也就没有原型。

手动创建的原型链

1.new 构造调用

原型链是支持修改的,默认情况下,new 构造调用返回的对象的[[Prototype]]指向他的构造函数。

function createObject () {
  this.a = 1;
  this.b = 2;
}
// obj => createObject.prototype => Object.prototype => null
const obj = new createObject();
console.log(Object.getPrototypeOf(obj));// {}
console.log(Object.getPrototypeOf(obj) === createObject.prototype);// true
console.log(Object.getPrototypeOf(createObject.prototype) === Object.prototype);// true

2.Object.create()

const a = { text: 'a' };
// b => a => Object.prototype => null
const b = Object.create(a);
console.log(b.text);// a
console.log(Object.getPrototypeOf(b));// { text: 'a' }
console.log(Object.getPrototypeOf(b) === a);// true
console.log(Object.getPrototypeOf(a) === Object.prototype);// true

3.Object.setPrototypeOf()

const c = { text: 'c' };
const d = { text: 'd', num: 123 };
// c => d => Object.prototype => null
Object.setPrototypeOf(c, d)// 把c的原型设置为d
console.log(c.num);// 123
console.log(Object.getPrototypeOf(c));// { text: 'd', num: 123 }
console.log(Object.getPrototypeOf(c) === d);// true
console.log(Object.getPrototypeOf(d) === Object.prototype);// true

4.__proto__

const e = { name: 'e' };
const f = { text: 'f', num: 123 };
Object.prototype.hello='hi'
// c => d => Object.prototype => null
e.__proto__ = f// 把e的原型设置为f
console.log(e);// { name: 'e' }
console.log(e.num);// 123
console.log(e.text);// f
console.log(e.tree);// undefined (原型链上访问不到)
console.log(e.hello);// hi (原型链上的爷爷节点有这个属性)
console.log(Object.getPrototypeOf(e));// { text: 'f', num: 123 }
console.log(Object.getPrototypeOf(e) === f);// true
console.log(Object.getPrototypeOf(f) === Object.prototype);// true

原型链的设计初衷

从上面的例子可以得出,我们可以手动改动对象的原型。当访问对象的属性不存在的时候,会向依据原型链,一层一层的往上找,直到顶层的原型链也找不到,就返回undefined。

原型链设计的初衷是为了实现属性和方法的共享。比如多个数组对象能访问到同一个push函数。

__proto__是什么

__proto__其实是浏览器厂商提供的语法糖,可以理解为如下

Object.defineProperty(Object.prototype, "__proto__", 
{ 
    get: function() { 
        return Object.getPrototypeOf(this) 
    }, 
    set: function(o) { 
        Object.setPrototypeOf(this, o) return o; 
    } 
})

可以用它来访问和设置对象的原型。

constructor的含义

image.png

根据上面图可以看出,constructor默认指向对象的构造函数。用instanceof可以判断constructor指向是谁。

提到默认,一般就是可以修改的。

const a = '我是a'
const err = new Error('报错');

console.log(err instanceof Error)// true
console.log(err.constructor === Error)// true

err.constructor = a;
console.log(err.constructor === Error)// false

故而,判断对象是否是某个函数的实例,constructor并不是一个稳定合适的方案,而是应该用instanceof

总结

  • 基本类型在调用一些api时,JS引擎内部会自动把它转换成封装对象,再调用封装对象上或者它的原型链上绑定的方法。
  • 介绍了基本类型默认的原型链是如何设计的,以及四种创建、修改原型链的方法。
  • 原型链设计的初衷是为了实现属性和方法的共享
  • 原型链是针对对象来说的,因为函数也是对象,所以函数也有原型链,函数的原型用prototype指向,对象的原型链用[[Protype]]指向。
  • __prototype是浏览器提供的访问和修改原型的语法糖。
  • constructor默认指向的构造对象的函数,它是可以被修改的,判断对象是否是某个变量的实例应该用instanceof较为准确。