努力让学习成为一种习惯,自信来源于充分的准备
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
前言
关于原型、原型链的解读。网络上有许许多多优秀的资料文章。就我个人感受,大部分文章都是“按部就班”的从构造函数出发、基于继承的角度拓展。看完后大概知道了整条链路。但是总感觉容易遗忘,过段时间再翻出下面这张图看,还是会皱眉头😕
我自己想了想有以下几个原因:
-
对一些概念模糊不清,没有深刻理解,比如:
__proto__、prototype、constructor(很多文章并没有详细介绍这几个概念,更多的类似是:“每个对象都会有一个_proto__属性,这个属性会指向该对象的原型,每个函数会有一个prototype属性,是调用该构造函数而创建的实例的原型等”),看完后脑子里只是有了一个简单的关系图谱。但是并没有本质理解 -
没有历史背景介绍,js为什么引入这么一套机制。(其实我们也知道,js其实是没有
继承这个概念的,它并不像java,无论是构造函数还是es6新出的class语法。都是在模仿,从语法层面看起来更加接近继承。但其实本质并不是) -
一些主要的困惑点(著名的先有🐔还是先有🥚问题)并没有一个合理的解释(更多的是js就是这么设计的类似)
为了解决我自己内心深处仍旧存在的疑惑。故有此篇文章。也希望加深小伙伴们对原型、原型链的理解。如果能帮助小伙伴们解决类似的困惑那再好不过了。那么让我们开始吧
[[prototype]]与__proto__
我们先来看一段代码
const obj = {}
console.log(obj.__proto__ === Object.prototype); // true
function Func() {}
const f = new Func()
console.log(f.__proto__ === Func.prototype); // true
每一个对象都有一个内置属性[[prototype]],可以通过__proto__属性访问(无论这个对象是通过字面量创建还是通过构造函数创建)
这里有个容易产生困惑的点:每一个对象都会有一个__proto__属性。这并不准确,__proto__属性是定义在Object.prototype上的,基于原型链的查找规则。这样我们直接通过对象访问也可以获取
我们可以验证下:
const obj = {}
console.log(Object.hasOwn(obj, '__proto__')); // false
const arr = []
console.log(Object.hasOwn(arr, '__proto__')); // false
console.log(Object.hasOwn(Object.prototype, '__proto__')) // true
这里可能有小伙伴会有疑问了🤔:Object.prototype.__ proto__不是为null吗?代表着整个原型链的尽头,我们访问xx.__proto__可不是null
我们来看下规范对__proto__的定义:(规范链接proto)
当我们获取Object.prototype.__proto__的时候会触发[[Get]]。内部调用了[[getPrototypeOf]],而其中的调用对象为this value就是我们操作的对象。返回的是对象的内部属性[[prototype]]
好了,这里我们可以明白了:__proto__是Object.prototype的一个访问器属性,由 getter 和 setter 函数组成,基于原型链的规则可以让我们直接在对象里访问,返回的是该对象的内部属性[[prototype]],如果在该对象上没有找到,则一直向上查找。最终查找到Object.prototype,可见[[prototype]]机制就是对象中的一个内部链接引用另一个对象
我们可以用代码表示__proto__的访问机制:
Object.defineProperty(Object.prototype, "__proto__", {
get() {
return Object.getPrototypeOf(this)
},
set(o) {
return Object.setPrototypeOf(this, o)
}
})
不再推荐使用__proto__。虽然一些浏览器仍然支持它,但也许已从相关的 web 标准中移除,也许正准备移除或出于兼容性而保留, 更推荐使用
Object.getPrototypeOf访问
为什么不推荐使用__proto__呢,因为它“不靠谱”
__proto__可以被改变,不再作为[[prototype]]的访问器
const obj = {}
Object.defineProperty(obj, '__proto__', {
value: 1
})
console.log(obj.__proto__) // 1
const obj = {}
Object.defineProperty(Object.prototype, '__proto__', {
value: 1
})
console.log(obj.__proto__) // 1
null原型对象不从Object.prototype继承任何属性,包括__proto__访问器属性。实际上无论[[prototype]]是什么,obj.__proto__始终返回undefined
const obj = Object.create(null)
console.log(obj.__proto__) // undefined
console.log(Object.getPrototypeOf(obj)) // null
const obj2 = {}
Object.setPrototypeOf(obj2,obj)
console.log(obj2.__proto__) // undefined
console.log(obj2.toString) // undefined
console.log(Object.getPrototypeOf(obj2)) // {}
因此,更加推荐使用getPrototypeOf访问对象的[[prototype]]
同样设置原型推荐使用setPrototypeOf而不是obj.__proto = xxx的形式(不管是哪种方式,都不推荐修改对象的[[prototype]],这会极大的影响性能)
prototype
函数(准确的说应该是函数实例)都有prototype属性,无论它是不是构造函数,也有一些情况的函数没有prototype属性
// 箭头函数
const func1 = () => {}
console.log(func1.prototype) // undefined
// 被bind绑定的函数
function foo() {}
const _foo = foo.bind()
console.log(_foo.prototype) // undefined
当
Function实例作为构造函数与new运算符一起使用时,该实例的prototype数据属性将用作新对象的原型
所谓的构造函数其实是通过new运算符调用的函数,new操作符内部会做一系列的处理(内部其实也是对prototype做了引用处理)
在这里,我们抛开一切概念,只需要记住一个基本点:函数的prototype属性引用的是一个对象
它的作用是什么呢,我们来举一个例子:
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
const p1 = new Person('jack', 24)
p1.sayName() // jack
const p2 = new Person('bob', 24)
p2.sayName() // bob
这里我们可以很明显的发现sayName方法在每一个实例上都要定义一遍。无论是从代码层面还是内存层面,这种写法都不太合适,一个很自然的想法是把可以复用的方法抽离出来
function sayName() {
console.log(this.name)
}
function Person(name, age) {
this.name = name
this.age = age
this.sayName = sayName
}
const p1 = new Person('jack', 24)
p1.sayName() // jack
const p2 = new Person('bob', 24)
p2.sayName() // bob
这样貌似解决了复用的问题,但会污染全局作用域,于是基于原型模式的解决方法来了
function Person(name) {
this.name = name
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const p1 = new Person('jack')
p1.sayName() // jack
const p2 = new Person('bob')
p2.sayName() // bob
那么,实例是怎么和构造函数的prototype联系起来的呢(也就是实例怎么才能访问到sayName方法呢)。在前面我们提到过,每一个实例对象都有一个[[prototype]]内部属性,它引用的就是其对应构造函数的prototype,这样一来,我们既解决了代码复用的问题,也解决了全局作用域的问题
其实到这里我们可以隐隐约约发现:这种查询机制与其说是继承,倒不如说是委托。当访问一个属性时,如果自身不存在该属性。则委托另外一个对象去查询,同时委托的对象也有委托的对象,一直到 Object.prototype,Object.prototype的委托对象是null
我们可以用下图来描述下三者的关系
用代码表示三者的关系
Object.getPrototypeOf(p1) === Person.prototype // true
Object.getPrototypeOf(Person.prototype) === Object.prototype // true
Object.getPrototypeOf(Object.prototype) === null // true
constructor
函数的prototype(实例原型对象),有一个constructor属性,返回一个引用,指向创建该实例对象的构造函数(即构造函数本身)
基于上面例子我们用代码输出看下
Person.prototype.constructor === Person // true
p1.constructor === Person // true 这里实例对象本身并没有constrcutor属性,最后还是基于 Person.constructor
Object.getPrototypeOf(Person.prototype).constructor === Object // true
我们进一步拓展原型链图
p1.constructor === Person,这段代码起码会有很大的误导性,认为p1对象是Person函数构造出来的,但其实不是。它是通过委托的方式访问。实际上访问的是Person.prototype的属性
但是constructor并没有多大的参考价值,因为它可以被随意改变,依旧基于上面的例子
Person.prototype.constructor = 2
console.log(p1.constructor) // 2
console.log(Person.prototype.constructor) // 2
另外有一个需要注意点:
当需要一次性为函数prototype属性添加多个方法的时候,我们可能会采用下面这种做法
Person.prototype = {
A() {},
B() {}
}
但是这样constructor会丢失,在下面这个例子中会指向Object
function Person(name) {
this.name = name
}
Person.prototype = {
sayName() {}
}
console.log(Person.prototype.constructor === Object); // true
如果需要纠正,我们需要手动添加constructor属性
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: true,
writable: true,
configurable: true,
value: Person
})
console.log(Person.prototype.constructor === Person); // true
可见constructor是一个不可靠且不太安全的引用。我们只需要明白:constructor并不代表对象被它构造
函数的[[prototype]]
上面我们知道了,函数的prototype属性是一个对象,引用了构造函数生成实例的原型。那么函数本身是否也有[[prototype]]呢。答案是肯定的
任何构造函数(Array、Object)都是通过new Function的形式创建的。它们都是通过Function构建的实例,它们的原型都指向Function.prototype,即所有函数的[[prototype]]属性指向Function.prototype
Object.getPrototypeOf(Array) === Function.prototype // true
Object.getPrototypeOf(Object) === Function.prototype // true
const func = function() {}
Object.getPrototypeOf(func) === Function.prototype // true
相信到这里,大家理解起来都没有问题
接下来便是万恶之源,也是整个原型链中最难理解、最绕的知识点
- Function构造函数怎么会自己创建自己,它既是实例又是构造函数
Object.getPrototypeOf(Function) === Function.prototype // true
- Function.prototype.[[prototype]] 为什么指向了Object.prototype, 然而前面又说 Object是基于Function创建的,这就涉及到"到底是先有鸡还是先有蛋的问题"
Object.getPrototypeOf(Function.prototype) === Object.prototype // true
相信上面两点是绝大多数小伙伴理解原型链的最大困惑点,关于这里的接受网上也有很多优秀的文章,在这里我们可以换个角度来思考:这样设计的目的是什么
首先,让我们忘记构造函数、继承、实例对象等一切概念,我们需要记住的是:js只有对象,所谓的原型链本质就是对象内部的一个属性引用了另外一个属性,仅此而已。没有继承,只有对象之间的引用
所有函数(这里不包括特殊情况,前面已经提到了)包括Function都通过Function构造出来的这句话可以翻译成:Function.prototype存放了很多函数使用的通用方法.所有的函数内部都有一个[[prototype]]属性引用了Function.prototype
以及我们经常使用的技巧:
Array.prototype.concat.apply
Array.prototype.slice.apply
同理,Object.prototype作为原型链的顶层,存放了很多通用的方法:toString、valueOf等。有些场景,函数也需要调用这些方法
怎么做呢。通过
委托的方式,所以Function.prototype内部需要有一个属性即[[prototype]]引用Object.prototype。这样才可以使用定义在其身上的方法
我们可以发现,这是一个很有用且有必要的设计。设计原型链的目的是什么:是为了更高效的组织代码,无论是从性能还是维护角度,这是一种高效的设计模式(对象之间的引用)
我们压根就不需要去纠结“到底是现有🐔还是先有🥚,自己怎么还可以创建自己”这些问题
我们再完善一下原型链图,这就是整个原型链图啦
最后
我们可以发现无论是new,还是 constructor甚至于es6中的class语法糖。都在刻意的模仿类的行为。让js这门语言更像一门面向对象的语言。究其原因为了让传统面向对象(比如 c++,Java 或 C#的程序员学习JavaScript的时候更容易理解,也就是为了让js更加畅销,具体可以参考javascript真的需要类吗
然后js本质上并没有类、继承这些概念。js只有对象。js通过委托的方式,逐层往上获取属性,直到尽头。
[[prototype]]机制就是指对象内部的一个属性引用另外一个对象。而原型链就是各个关联对象产生的链条。关联对象之间通过[[prototype]]访问
到这里,就是本篇文章的全部内容了
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
如果你有疑问或者出入,评论区告诉我,我们一起讨论