前言
大家好,我是抹茶。
原型链一直都是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.prototype,String.prototype的原型是Object.prototype。
对比str 和 str1 两种变量声明的方式,可以看到new 构造调用返回的是一个对象,而[[Prototype]]是对象上的内置属性。
上面也提到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的含义
根据上面图可以看出,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较为准确。