前文——深入理解JavaScript-原型(一)
中间这段文章发不出来,不知道为什么
Object.setPrototypeOf
此方法设置一个特定的对象的原型到另一个对象或 null。ES6 新增方法。语法为:Object.setPrototypeOf(obj, prototype)
。具体例子为:
const obja = { a: 1 }; const objb = { b: 1 }; Object.setPrototypeOf(obja, objb); console.log(obja);
能看出,我们通过 Object.setPrototypeOf 方法,将 objb 设置为 obja 的原型,打印 obja 时,我们能看到 obja 的隐式原型([[Prototype]])指向 objb
除此之外,还有一个方法能显式继承原型——Object.create
Object.create
它用于创建一个新对象,使用现有的对象作为新创建对象的原型。ES5 新增方法。它的语法是 Object.create(proto, propertiesObject)
。可看案例:
const obja = { a: 1 }; const a = Object.create(obja); console.log(a);
笔者在此之前曾写过一篇文章——Object.create,介绍它是如何实现的,其底层就是用到了原型继承
我们对 Object.setPrototypeOf 和 Object.create 进行对比
- Object.setPrototypeOf,给我两个对象,将其中一个设置为另一个的原型
- Object.create,给我一个对象,将它作为我所创建的新对象的原型
从发展的角度看,Object.create 是 ES5 出现,但它不满足两个对象设置原型时,ES6 就提供了新的方法——Object.setPrototypeOf。但无论如何,它们都是后来因为个别需求而新增的 API,无法与 JavaScript 一开始采用的隐式继承相媲美
隐式原型继承
这里,我们不妨温习一下 new 实现原理:
- 创建一个新对象
- 设置该对象的 [[Prototype]] 为构造函数 prototype 属性
- 将构造函数内部的 this 赋值给该对象
- 执行构造函数的内部代码
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
这就是隐式继承,只要我们使用 new 创建对象,就会执行以上步骤,当然,用对象字面量是有一样的隐式操作
var obj = {}; // === new Object() 对象字面量 var arr = []; // === new Array() 数组字面量 function func() {} // new Function(); 函数字面量 var str = '123'; // === new Srting('123') 字符串字面量 var bool = true; // === new Boolean(true) 布尔字面量 // ...
JavaScript 提供了几个内置的构造函数,如 Object、Array、Boolean、String、Number 等等,当我们使用 {}
、[]
、function
等符号或关键字时,就是在执行与new
操作一样的隐式原型继承
我们可以这样说:隐式原型继承的目的是方便开发者更简洁的实现继承
隐式原型继承和显式原型继承的互相转换
无论是隐式原型继承,还是显式原型继承,都是对对象隐式引用的应用。两者之间具有一定的互操作性,也就是说,用其中一个,能实现另一个的部分行为
例如我们用隐式原型继承实现 Object.create,实现上是手写 Object.create 方法,正如我们在 Object.create 中曾实现的那样:
function create(proto) { function F() {} F.prototype = proto; return new F(); }
原理也很简单,创建一个函数,将它的原型赋值为目标对象,在实例化这个函数,返回实例化后的值。new 实例化,相当于实例化的值的 [[Prototype]] 指向了目标对象
function create(proto) { function F() {} // 创建一个函数,每个函数都有一个 prototype 属性,指向原型对象 F.prototype = proto; // 原本的 prototype 是一个对象,其中有两个属性,一个是 constructor,即构造函数,指向 F;另一个为 [[Prototype]],指向 Object.prototype // 现在将它赋值为 proto return new F(); // new的作用是创建空对象,将该对象的原型赋值为另一个构造函数的 prototype 属性,并执行该构造函数 // 所以 new F() 后的实例的的[[Prototype]]指向 F.prototype,也就是传入的 proto }
以上,我们就用 new 实现了显式原型继承
那么如何用显式原型继承实现 new (或者对象字面量)呢
我们在手写 new 中已经实现过,这里贴上代码:
function new2(Constructor, ...args) { var obj = Object.create(null); // 创建一个对象 obj.__proto__ = Constructor.prototype; // 将新对象的 [[Prototype]] 属性赋值为构造函数的原型对象 var result = Constructor.apply(obj.args); // this赋值新对象并初始化 return typeof result === 'object' ? result : obj; // 非空返回结果 }
这也解释了面试时常考的两个面试题:手写 new 和手写 Object.create,两者一个是隐式原型继承,另一个是显式原型继承,两者间能通过各自特性实现对方方法
总结
如此,我们就明白了原型是什么,原型与原型链的关系等等。而且我们知道 「JavaScript 是基于原型继承的语言」这句话的涵义。
虽然现在原型在面试中已经显得不重要,知乎上曾有过这样的问题——面试一个 5 年的前端,却连原型链也搞不清楚,满口都是 Vue,React 之类的实现,这样的人该用吗?。不懂原型照样开发业务也很正常。笔者觉得这并不奇怪,毕竟前端的前端开发已经从面向对象转向函数式编程
在文末回答下文章开头的问题
Q:原型是什么?
A:给其他对象提供共享属性的对象
Q:为什么要有原型?
A:JavaScript 是基于原型继承的语言,在这里是为了实现继承
Q:prototype 和 __proto__
有什么区别
A:prototype 是函数特有的属性,每个函数创建时都会自带 prototype 属性,它指向一个对象,这个对象叫做原型对象。而每个对象都有一个 __proto__
属性,它也指向原型对象。如果说两者有什么关系?那么 子对象的__proto__
=== 父对象的 prototype
Q:原型链又是什么
A:每个对象都有 __proto__
属性,它指向原型对象,原型对象也是对象,也有 __proto__
属性,并指向它的原型对象,这样一层一层,最终指向 null,这种关系被称为原型链。
Q:原型是如何实现继承的
A:原型继承有四种方法,以是否手动操作为依据分为隐式原型继承和显式原型继承。隐式原型继承在我们开发中占大多数,即对象字面量和 new,即这两种方法语言底层会帮我们实现创建对象、关联原型和属性初始化。显式原型继承分为 Object.create 和 Object.setPrototypeOf,它能主动设置某个对象为另一个对象的原型
Q:原型和原型链的关系如何
A:原型是实现继承的方法,原型链是继承的产物
参考资料
- 深入理解 JavaScript 原型
- 如何回答面试中的 JavaScript 原型链问题