我们不妨先考察一下 prototype
所依赖的其他概念。
定义
对象
-
member of the type Object
Object 类型的成员
-
An object is a collection of related data and/or functionality
对象是一个包含相关数据和
方法功能的集合对象可以用如下方法生成
const o1 = {} const o2 = new Object() const o3 = Object.create(Object.prototype)
函数
-
member of the Object type that may be invoked as a subroutine
Object 类型的成员,可做为子程序被调用。
-
一般来说,一个函数是可以通过外部代码调用的一个“子程序”(或在递归的情况下由内部函数调用)……简而言之,它们是Function对象。
类似于对象,函数可以用如下方法生成
const f1 = (x) => { console.log(x) } // 特别的,f1.prototype === undefined const f2 = function(x) { console.log(x) } const f3 = new Function("x", "console.log(x)") // 不推荐 const f4 = Function("x", "console.log(x)") // 不推荐 // const f5 = Object.create(Function.prototype) 并不会生成一个函数
构建函数
-
function object that creates and initializes objects
创建并初始化对象的函数对象
-
constructors provide the means to create as many objects as you need in an effective way, attaching data and functions to them as required
构建函数提供了创建您所需对象
实例的有效方法,将对象的数据和特征函数按需联结至相应正在创建的这些对象。
一般来讲,任何函数都有机会成为构建函数,但依然有反例,比如 Function.prototype
,ECMAScript 的解释如下:
The Function prototype object is the intrinsic object %FunctionPrototype%. The Function prototype object is itself a built-in function object. When invoked, it accepts any arguments and returns
undefined
. It does not have a [[Construct]] internal method so it is not a constructor.Function 的原型对象是内部对象 %FunctionPrototype%。Function 原型对象本身是一个内置的函数对象。当其被调用时,不论接受任何参数都将返回
undefined
,他没有 [[Construct]] 内部方法,因此他不是构造函数。
一个更广泛的例子是箭头函数,所有的箭头函数都没有 [[Construct]] 内部方法。
[[Construct]]
ECMAScript 表6第二项
Creates an object. Invoked via the
new
orsuper
operators. ... ... Objects that implement this internal method are called constructors. A function object is not necessarily a constructor and such non-constructor function objects do not have a [[Construct]] internal method.(用于)创建对象。通过
new
或super
运算符调用。实现此内部方法的对象称之为构建函数。函数对象不一定是构造函数,此类非构造函数对象没有 [[Construct]] 内部方法。
对象的内部方法(Objects Internal Methods)
既然提到了 [[Construct]],我们不妨大概浏览一下什么叫做内部方法。
The actual semantics of objects, in ECMAScript, are specified via algorithms called internal methods. Each object in an ECMAScript engine is associated with a set of internal methods that defines its runtime behaviour. These internal methods are not part of the ECMAScript language. They are defined by this specification purely for expository purposes. However, each object within an implementation of ECMAScript must behave as specified by the internal methods associated with it. The exact manner in which this is accomplished is determined by the implementation.
ECMAScript 中对象的实际语义是通过称为内部方法的算法指定的。ECMAScript 引擎中的每个对象都与一组定义其运行时行为的内部方法相关联。这些内部方法不是 ECMAScript 语言的一部分。它们仅由出于说明目的而由本规范定义。但是,ECMAScript 所实现的每个对象必须按照与之关联的内部方法所指定的行为去运作。完成此操作的确切方式由(ECMAScript 引擎的)具体实现决定。
如果觉得不好理解可以参考这里。总而言之,内部方法是由引擎实现的,他可以是由任何语言(比如 C++)实现的;实现的具体细节可以和 ECMAScript 所详细定义的不同,但是只要行为一致就可以了。
Object.create
-
The
Object.create()
method creates a new object, using an existing object as the prototype of the newly created object.方法
Object.create()
创建一个新对象,使用一个现有的对象(做参数)作为这个新对象的原型(指__proto__)。 -
The
create
function creates a new object with a specified prototype.函数
create
创建一个新对象,并指明他的原型。其实现如下:
Object.create = function(prototype, properties) { if (prototype === undefined || prototype === null) { throw new TypeError(); } let obj = ObjectCreat(prototype) if (arguments.length > 1 && properties !== undefined) { return ObjectDefineProperties(obj, properties) } return obj }
换言之,如果不传
Properties
,等同于调用ObjectCreate(O)
。The abstract operation ObjectCreate ... is used to specify the runtime creation of new ordinary objects.
抽象操作 ObjectCreate 用于在运行时定义一个新的普通对象(ordinary objects)。
其实现相当于:
function ObjectCreate(proto, internalSlotsList) { if (arguments.length === 1) { internalSlotsList = [] } let obj = {} internalSlotsList.forEach(internalSlotName) { // set obj.[[ [internalSlotName] ]] as 9.1 } obj.[[Prototype]] = proto obj.[[Extensible]] = true return obj }
其中有关 [[Extensible]] 的解释可以看这里,大意是 [[Extensible]] 可以控制一个对象能不能被添加新的属性;同时若 [[Extensible]] 为
false
,则不能修改 [[Prototype]] 的值。
综上,Object.create
在没有第二个参数 properties
时其实等同于 a = { __proto__: b }
。
new
-
The new operator lets developers create an instance of a user-defined object type or of one of the built-in object types that has a constructor function.
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例
当然,对于运算符,更重要的是其行为而非定义,MDN 中对
new
的行为解释如下:当代码 new Foo(...) 执行时,会发生以下事情:
-
一个继承自 Foo.prototype 的新对象被创建。
-
使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
-
由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)
也就是说,如果将
const f1 = new Foo(1)
写作
const f1 = EvaluateNew(Foo, [1])
则可视为有:
function EvaluateNew(constructorFunction, args) { const instance = Object.create(constructorFunction.prototype) // 步骤 1. const tempResult = constructorFunction.apply(instance, args) // 步骤2. return isObject(tempResult) ? tempResult : instance // 步骤3. } function isObject(value) { return Object(value) === value; }
-
在 ECMAScript 中的阐述,其总体上和上文 MDN 所述的过程一致。
ReturnIfAbrupt
在前文各抽象操作流程中,都有操作 ReturnIfAbrupt
的影子,参考阮一峰的 ES6 入门教程的相关介绍。阮将 ReturnIfAbrupt
解释为:
如果有报错,就返回错误,否则取出值
言外之意,在 ReturnIfAbrupt()
操作执行之前,即使某个操作的结果代表了有错误或中断发生,也不会立即执行。
prototype
object that provides shared properties for other objects
为其他对象提供共享属性的对象
当构造器创建一个对象,为了解决对象的属性引用,该对象会隐式引用构造器的“prototype”属性。通过程序表达式 constructor.prototype 可以引用到构造器的“prototype”属性,并且添加到对象原型里的属性,会通过继承与所有共享此原型的对象共享。另外,可使用 Object.create 内置函数,通过明确指定原型来创建一个新对象。
虽然定义指出原型是一个对象,但是 Function
似乎不这么认为,因为 typeof Function.prototype === 'function'
作为一个属性,只有函数类型的变量才会拥有 prototype
。
__proto__
const Foo = function() {}
const f1 = new Foo()
f1.__proto__ === Foo.prototype // true
上文操作符 new
可知
如果存在 const f1 = new Foo()
, 则会有 f1.__proto__ === Foo.prototype
;
也就是说一个变量 f1 的 __proto__
,就是生成 f1 的构造函数 Foo 的 prototype
。
实际上 __proto__
已经成为一个非标准的属性了,不过目前大多数浏览器还是支持这个属性,ECMAScript 2015 提出使用 Object.getPrototypeOf
作为标准方法来寻找 __proto__
,就如:
Object.getPrototypeOf(Foo) === Function.prototype // true
但从语义上讲这就像是个令人困惑的绕口令——“Foo 的 prototype 不是 Foo.prototype 而是 Function.prototype”。
实际上,访问 __proto__
的行为注册在 Object.prototype
上,具体实现如下:
Object.defineProperty(Object.prototype, '__proto__', {
get() {
let _thisObj = Object(this);
return Object.getPrototypeOf(_thisObj);
},
set(proto) {
if (this === undefined || this === null) {
throw new TypeError();
}
if (!isObject(proto) || proto === null) {
return undefined;
}
if (!isObject(this)) {
return undefined;
}
let status = Reflect.setPrototypeOf(this, proto);
if (!status) {
throw new TypeError();
}
},
});
function isObject(value) {
return Object(value) === value;
}
这也解释了为什么 Object.create(null)
生成的对象没有 __proto__
属性,因为 Object.prototype
不在他的原型链上。
Reflect.has(Object.create(null), '__proto__') // false
参见:
prototype
与 __proto__
理清了所有的概念之后,我们回过头来看 prototype
与 __proto__
。
首先我们先来单纯的考察一下 f1
有关的 prototype
与 __proto__
。
上图中八边形代表 object 类型变量,圆角矩形代表 function 类型变量;实线代表属性,虚线代表关系。
三者的关系一目了然。但从上文的各种定义来看,f1.__proto__ = Foo.prototype
是 new
的功能,那么 Foo.prototype
又是何时被谁创建的呢?
我们考察一下 Foo.__proto__
:
其中 ƒ () { [native code] }
代表的含义可以参见这里
显然我们可以合理的推测,“Foo.prototype
是在执行 const Foo = new Function()
时,由 Function
创建的”
这里也有一个令人困惑的地方,我们会发现:
Object.getPrototypeOf(Function) === Function.prototype // true
那么我们能不能认为“Function
是由 Function
本身构建的”呢?
我认为答案是否定的。因为 Function
并不是经由 new Function()
操作产生的,他是一个内置对象(built-in object),如前文的所讲,Function
应当是由其内部方法 [[Construct]] 实现的,并不会存在诸如 const Function = new Function()
这种用户级别的代码去完成 Function
的构建。
Function
等内置对象的 prototype
与 __proto__
,完全是由 ECMAScript 规定,由引擎直接实现的。不应当把这些内置对象的 prototype
与 __proto__
与用户级的函数例如 Foo
的 prototype
与 __proto__
混为一谈。
混淆与命名
至此,prototype
与 __proto__
之间的关系应该已经被理清了。
他们总易于混淆,是因为这两个属性一般都被翻译成“原型”。对于由函数 Foo
生成的变量 f1
来说,f1 的原型是 f1.__proto__
,同时也是 Foo.prototype
,到此为止还是很好理解;但与此同时 Foo.prototype
和 Foo.__proto__
都被称为 “Foo的原型”,这就十分令人困惑了。
从自然语义上讲,这样的混淆是无法避免的,我们来看 prototype 的含义:
the first example of something, such as a machine or other industrial product, from which all later forms are developed
事物的第一个例子,例如机器或其他工业产品,所有后来的形式都是从中开发出的
显然,假如汽车博物馆收藏了五菱宏光的原型车,那么“五菱宏光这个型号的原型车”,和“我家上个月刚买的那台五菱宏光的原型车”,都指向博物馆的那辆馆藏,是完全合理的。
既然自然语义无法满足区分的需要,那么就需要新的语义去做区分。实际上 ECMAScript 的规范已经尝试做了区分:
- 将
Foo.prototype
称之为 “Foo 原型对象”(Foo prototype object) 或者 “Foo 的 ‘prototype
’ 属性”(Foo's 'prototype
' property) - 将
Foo.__proto__
称之为 “Foo 的原型”(the Foo's prototype)
在记忆上,只要了解上文中阐述的 prototype
与 __proto__
产生的时机,就能方便的区分开来了。一般的,对于用户级的代码:
prototype
创建于Function
被调用时,即执行Function()
__proto__
“创建”于new
运算符运算中,执行Object.create(Foo.prototype)
时
对于内置对象,则 prototype
与 __proto__
都是按 ECMAScript 规范所特指的。
ECMAScript 中的操作符 `New`
ECMAScript 其中规定:
- 对于语法
new NewExpression
,有EvaluateNew(NewExpression, empty)
- 对于语法
new MemberExpression Arguments
,有EvaluateNew(MemberExpression, Arguments)
function EvaluateNew(constructProduction, argumentsList) {
let constructor = GetValue(constructProduction)
let argList = argumentsList || []
if (!IsConstructor(argument)) {
throw new TypeError();
}
return Construct(constructor, argList)
}
// http://www.ecma-international.org/ecma-262/6.0/#sec-construct
function Construct(F, argumentsList, newTarget) {
if (Reflect.has(arguments, 2)) {
newTarget = F
}
if (Reflect.has(arguments, 1)) {
argumentsList = []
}
// 可以断言这里会有:IsConstructor(F) === true && IsConstructor(newTarget) === true
return F.[[Construct]](argumentsList, newTarget)
}
// http://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-function-objects-construct-argumentslist-newtarget
function F.[[Construct]](argumentsList, newTarget) {
// 可以断言这里 F 是一个函数对象,isObject(newTarget) === true
// 关于运行时上下文的解释见 http://www.ecma-international.org/ecma-262/6.0/#sec-execution-contexts
// 和我们通常理解的“使用栈维护的运行时上下文”是一致的
let callerContext = ExecutionContentStack.peek()
// [[ConstructorKind]] 的类型只会有 "derived" 或 "base"
// 若如 Construct 中的断言所说,IsConstructor(F) === true,那么必然会有 F.[[ConstructorKind]] === "base"
let kind = F.[[ConstructorKind]]
let thisArgument = undefined
if (kind === 'base') {
// 从 Construct 中可知,此处 newTarget === F
// http://www.ecma-international.org/ecma-262/6.0/#sec-ordinarycreatefromconstructor
thisArgument = Object.create(newTarget.prototype)
}
// [A]约等于
// const result = F.apply(thisArgument, argumentsList)
// TODO
// [A] start
let calleeContext = PrepareForOrdinaryCall(F, newTarget)
// 可以断言这里 calleeContext 一定是 this
if (kind === 'base') {
OrdinaryCallBindThis(F, calleeContext, thisArgument)
}
let constructorEnv = calleeContext.LexicalEnvironment
let envRec = constructorEnv.EnvironmentRecord
try {
let result = OrdinaryCallEvaluateBody(F, argumentsList)
} catch (e) {
throw e
} finally {
// 根据 .14 "Else, ReturnIfAbrupt(result)",OrdinaryCallEvaluateBody 即使报错了也会重置当前运行上下文
ExecutionContentStack.remove(calleeContext)
let callerContext = ExecutionContentStack.peek()
}
// [A] end
if (result !== undefined) {
if (isObject(result)) {
return NormalCompletion(result)
}
if (kind === 'base') {
return NormalCompletion(thisArgument)
}
throw TypeError()
}
return envRec.GetThisBinding()
}
// http://www.ecma-international.org/ecma-262/6.0/#sec-ordinarycreatefromconstructor
function OrdinaryCreateFromConstructor(constructor, intrinsicDefaultProto, internalSlotsList) {
let proto = constructor.prototype
return Object.create
}
function GetValue() {
// TODO
}
function IsConstructor(argument) {
if (!isObject(argument)) {
return false
}
// 检查 argument 拥有内部方法 [[Construct]]
// 当然内部方法实际上是无法被这样检查的,这里只作示意
if (Reflect.has(argument, [[Construct]])) {
return true
}
return false
}
function isObject(value) {
return Object(value) === value;
}
前面我们试图理清了 prototype
与 __proto__
的关系,这里试图探讨一下 JavaScript 中的原型链与继承,以及这种继承的实现与基于类的其他语言有什么异同。
原型链的简单例子
参考 MDN 的这篇教程
let Foo = function() {
this.x = 1
this.y = 2
}
let f1 = new Foo() // {x: 1, y: 2}
Foo.prototype.y = 3
Foo.prototype.z = 4
此时,我们可以得到一条原型链:
Foo {x: 1, y: 2}
---> {y: 3, z: 4, constructor: ƒ}
---> Object.prototype
---> null
我们可以考察一下对于 f1
,他的属性是如何被找到的
console.log(f1.x)
// x 是 f1 的属性吗?是的,该属性的值为 1
console.log(f1.y)
// y 是 f1 的属性吗?是的,该属性的值为 2
// 原型上的 y 将被遮蔽
console.log(f1.z)
// z 是 f1 的属性吗?不是
// z 是 f1.__proto__ 的属性吗?是的,该属性的值为 4
console.log(f1.k)
// z 是 f1 的属性吗?不是
// z 是 f1.__proto__ 的属性吗?不是
// z 是 f1.__proto__.__proto__,即 Object.prototype 的属性吗?不是
// f1.__proto__.__proto__.__proto__ 为 null,停止搜索,返回 undefined
对于方法的继承,也是类似的
let Foo = function(count) {
this.count = count || 0
this.addCount = function() {
this.count++
return this
}
}
let f1 = new Foo()
Foo.prototype.addCount = function() {
this.count += 2
return this
}
Foo.prototype.subtractCount = function() {
this.count -= 2
return this
}
此时,我们可以得到一条原型链:
Foo {count: 0, addCount: ƒ}
---> {addCount: ƒ, subtractCount: ƒ, constructor: ƒ}
---> Object.prototype
---> null
f1.addCount()
// Foo {count: 1, addCount: ƒ} f1.__proto__.addCount 被遮蔽
f1.subtractCount()
// Foo {count: -1, addCount: ƒ} 执行了 f1.__proto__.subtractCount
// 这里函数内的 this 会指向 f1 而非 f1.__proto__
所以我们得出结论,JavaScript 的原型链是这么工作的:
JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
继承
现在让我们回到面向对象上来。假设我需要实现两个类,Person 与 Teacher,显然 Teacher 应当继承自 Person,我们应该如何做呢?
不使用 class
为了针对性的展示原型链在继承中的作用,首先我们看一下不使用 class
的情况:
const Person = function(props = {}) {
const { firstName, lastName, age, gender } = props
this.name = { first: firstName, last: lastName }
this.age = age
this.gender = gender
}
Person.prototype.come = function () {
console.log(`${this.name.first} is coming.`)
}
Person.prototype.greeting = function() {
console.log(`Hi! I'm ${this.name.first}.`)
}
const p1 = new Person({
firstName: 'Ian',
lastName: 'Zhang',
age: 24,
gender: 'male'
})
p1.come() // Ian is coming.
p1.greeting() // Hi! I'm Ian.
这里,我们想给 Teacher 更新一个 greeting 函数,让他能进行更正式的自我介绍并增加一个属性——学科。
const Teacher = function(props = {}) {
Person.call(this, props)
this.subject = props.subject
}
const t1 = new Teacher({
firstName: 'Ian',
lastName: 'Zhang',
age: 24,
gender: 'male',
subject: 'CS',
})
但是这时我们会发现,t1 既不会打招呼,也不会走过来:
t1.coming() // Uncaught TypeError: t1.coming is not a function
t1.greeting() // Uncaught TypeError: t1.greeting is not a function
这时,就需要我们连接他们的原型链了,考察以下方法:
[A]
Teacher.prototype = Object.create(Person.prototype)
Teacher.prototype.constructor = Teacher
// "标准"方法
// Object.create(Person.prototype) 实际上会生成一个对象
or [B]
Object.assign(Teacher.prototype, Object.create(Person.prototype))
// 这个方法无效
// Object.create(Person.prototype).__proto__ 不可枚举,Object.assign 会将其忽略
// 属性描述符参见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#属性描述符
or [C]
Teacher.prototype = { constructor: Teacher, __proto__: Person.prototype }
// [C] 和 [A] 应该是完全相同的。当然这只是我的猜测
or [D]
Object.assign(Teacher.prototype, { __proto__: Person.prototype })
// 和 [B] 类似,这个方法无效
// __proto__ 不可枚举,Object.assign 会将其忽略
or [E]
Teacher.prototype = { ...Teacher.prototype, __proto__: Person.prototype }
// 假设 Teacher.prototype 仅有 constructor 和 __proto__ 两个属性
// [E] 和 [C] 是完全相同的
or [F]
Teacher.prototype.__proto__ = Person.prototype
// 可以影响到已经生成的实例 t1
or [G]
Object.setPrototypeOf(t1.__proto__ Person.prototype)
// 仅影响到已经生成的实例 t1
or [H]
t1.__proto__.__proto__ = Person.prototype
// 仅影响到已经生成的实例 t1
在这里我们选用标准写法 [A],之后生成新的 Teacher 实例
const t2 = new Teacher({
firstName: 'Rohry',
lastName: 'Snow',
age: 22,
gender: 'female',
subject: 'biology',
})
t2.greeting() // Hi! I'm Rohry.
t2.come() // Rohry is coming.
之后我们可以给 Teacher 的原型对象增加方法,以屏蔽 Person 原型对象中的方法
Teacher.prototype.greeting = function() {
console.log(`Hello. My name is Mx. ${this.name.last}, and I teach ${this.subject}.`)
}
t1.greeting() // Hello. My name is Mx. Zhang, and I teach CS.
t2.greeting() // Hello. My name is Mx. Snow, and I teach biology.
使用 class
ECMAScript6 引入了一套新的关键字用来实现 class,对于一开始就使用 ES5/6 或者使用基于类语言的开发者来说,更熟悉这种写法。
class Person {
constructor(props) {
const { firstName, lastName, age, gender } = props
this.name = { first: firstName, last: lastName }
this.age = age
this.gender = gender
}
come = () => {
console.log(`${this.name.first} is coming.`)
}
greeting = () => {
console.log(`Hi! I'm ${this.name.first}.`)
}
}
class Teacher extends Person {
constructor(props) {
super(props)
this.subject = props.subject
}
greeting = () => {
console.log(`Hello. My name is Mx. ${this.name.last}, and I teach ${this.subject}.`)
}
}
实际上 class
就是构造函数的语法糖,两者基本等价。两者也可以混用,也是没有问题的。
但是你会发现,两种继承的实现方式有着微妙的不同,你会发现:
- 不使用
class
时:
Person.__proto__ === Function.prototype // true
Teacher.__proto__ === Function.prototype // true
- 使用
class
时:
Person.__proto__ === Function.prototype // true
Teacher.__proto__ === Person // true
这是由于,class
的继承是按照以下的模式实现的:
Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(B, A);
这是由于
综上,t1
的原型链如下图所示:
所以有关 Javascript 中的继承,引用别人的一句话来说就是:
……与其叫继承,委托的说法反而更准确些。
原文刊载于 Blog/01.JavaScript prototype 与 __proto__.md at master · TAUnionOtto/Blog · GitHub
作者:云网安全事业部 - 张乐萌