searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

JavaScript 原型链与继承

2023-05-16 03:11:29
13
0

我们不妨先考察一下 prototype 所依赖的其他概念。

定义

对象

  • ECMAScript

    member of the type Object

    Object 类型的成员

  • MDN

    An object is a collection of related data and/or functionality

    对象是一个包含相关数据和方法功能的集合

    对象可以用如下方法生成

    const o1 = {}
    const o2 = new Object()
    const o3 = Object.create(Object.prototype)
     

函数

  • ECMAScript

    member of the Object type that may be invoked as a subroutine

    Object 类型的成员,可做为子程序被调用。

  • MDN

    一般来说,一个函数是可以通过外部代码调用的一个“子程序”(或在递归的情况下由内部函数调用)……简而言之,它们是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) 并不会生成一个函数
     

构建函数

  • ECMAScript

    function object that creates and initializes objects

    创建并初始化对象的函数对象

  • MDN

    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.prototypeECMAScript 的解释如下:

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 or super 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]],我们不妨大概浏览一下什么叫做内部方法。

ECMAScript

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

  • MDN

    The Object.create() method creates a new object, using an existing object as the prototype of the newly created object.

    方法 Object.create() 创建一个新对象,使用一个现有的对象(做参数)作为这个新对象的原型(指__proto__)。

  • ECMAScript Object.create

    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)

    ECMAScript ObjectCreate

    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

  • MDN

    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(...) 执行时,会发生以下事情:

    1. 一个继承自 Foo.prototype 的新对象被创建。

    2. 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。

    3. 由构造函数返回的对象就是 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

ECMAScript

在前文各抽象操作流程中,都有操作 ReturnIfAbrupt 的影子,参考阮一峰的 ES6 入门教程的相关介绍。阮将 ReturnIfAbrupt 解释为:

如果有报错,就返回错误,否则取出值

言外之意,在 ReturnIfAbrupt() 操作执行之前,即使某个操作的结果代表了有错误或中断发生,也不会立即执行。

prototype

ECMAScript

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  

作者:云网安全事业部 - 张乐萌

0条评论
0 / 1000
张****萌
1文章数
0粉丝数
张****萌
1 文章 | 0 粉丝
张****萌
1文章数
0粉丝数
张****萌
1 文章 | 0 粉丝
原创

JavaScript 原型链与继承

2023-05-16 03:11:29
13
0

我们不妨先考察一下 prototype 所依赖的其他概念。

定义

对象

  • ECMAScript

    member of the type Object

    Object 类型的成员

  • MDN

    An object is a collection of related data and/or functionality

    对象是一个包含相关数据和方法功能的集合

    对象可以用如下方法生成

    const o1 = {}
    const o2 = new Object()
    const o3 = Object.create(Object.prototype)
     

函数

  • ECMAScript

    member of the Object type that may be invoked as a subroutine

    Object 类型的成员,可做为子程序被调用。

  • MDN

    一般来说,一个函数是可以通过外部代码调用的一个“子程序”(或在递归的情况下由内部函数调用)……简而言之,它们是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) 并不会生成一个函数
     

构建函数

  • ECMAScript

    function object that creates and initializes objects

    创建并初始化对象的函数对象

  • MDN

    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.prototypeECMAScript 的解释如下:

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 or super 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]],我们不妨大概浏览一下什么叫做内部方法。

ECMAScript

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

  • MDN

    The Object.create() method creates a new object, using an existing object as the prototype of the newly created object.

    方法 Object.create() 创建一个新对象,使用一个现有的对象(做参数)作为这个新对象的原型(指__proto__)。

  • ECMAScript Object.create

    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)

    ECMAScript ObjectCreate

    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

  • MDN

    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(...) 执行时,会发生以下事情:

    1. 一个继承自 Foo.prototype 的新对象被创建。

    2. 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。

    3. 由构造函数返回的对象就是 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

ECMAScript

在前文各抽象操作流程中,都有操作 ReturnIfAbrupt 的影子,参考阮一峰的 ES6 入门教程的相关介绍。阮将 ReturnIfAbrupt 解释为:

如果有报错,就返回错误,否则取出值

言外之意,在 ReturnIfAbrupt() 操作执行之前,即使某个操作的结果代表了有错误或中断发生,也不会立即执行。

prototype

ECMAScript

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  

作者:云网安全事业部 - 张乐萌

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0