JavaScript对象原型与原型链

原创
2020/08/03 03:01
阅读数 249

JavaScript 是动态的且没有静态类型,当谈到继承时,JavaScript 只有一种结构:对象。

在计算机科学中,对象(Object)可以是变量、数据结构、函数或方法。作为内存区域,对象包含一个值并由标识符引用。 在面向对象编程范式中,对象可以是变量、函数和数据结构的组合;特别是在基于类的范例变体中,对象指的是类的特定实例。 在数据库管理的关系模型中,对象可以是表或列,也可以是数据与数据库实体之间的关联(例如将人的年龄与特定人相关联)。

有这么一句话比较流行:“在 JavaScript 中,一切皆对象。”用“一切”来描述,这也太过绝对了吧?我的理解,这句话并不是说 JavaScript 中所有的东西都是对象,而是说 JavaScript 中的对象是 JavaScript 中的一个重要组成部分。

在 JavaScript 中,NumberStringBooleanBigIntSymbolObjectfunction等都可以按照对象的方式进行处理。nullundefined 以外原始类型都对应一个对象包装器。在一定条件下也会自动转为对象,也就是原始类型的“包装对象”,比如运算时隐式转换。换种方式可以理解为这些原始类型的字面量就是“包装对象”的实例,当然,instanceof` 并不这样认为。

除了 undefined,几乎都是对象,可以操作属性和方法。不可否认,null也是对象,只不过它是一个空对象,没有可操作的属性和方法。nullundefined 含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这是历史原因。

1995年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 表示"无"。根据 C 语言的传统,null 可以自动转为 0。但是,JavaScript 的设计者 Brendan Eich觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,如果 null 自动转为 0,很不容易发现错误。

因此,Brendan Eich又设计了一个 undefined。区别是这样的:null 是一个表示“空”的对象,转为数值时为 0undefined 是一个表示"此处无定义"的原始值,转为数值时为 NaN

JavaScript 中的对象分为两种:

  • 普通对象:通过 new 创建的对象,或者通过字面量创建的对象。
  • 函数对象:通过函数创建的对象。
// 新创建的对象以 Object.prototype 作为它的 [[Prototype]]
const literal = {}

// 数组继承了 Array.prototype(具有 indexOf、forEach 等方法)
const arr = []

// Object构造函数
let object = new Object({})

// Object.create静态方法
let create = Object.create({})

// 函数继承了 Function.prototype(具有 call、bind 等方法)
function Func () {}
Func.prototype.attr = {}
let func = new Func()

// 可以通过 __proto__ 字面量属性将新创建对象的
const proto = { b: 2, __proto__: o }

原型对象(prototype

原型是 JavaScript 对象相互继承特性的机制。

原型(Prototype)是为测试概念或流程而构建的产品的早期样本、模型或版本。它是一个在多种上下文中使用的术语,包括语义、设计、电子和软件编程。

JavaScript 是基于原型的语言,原型对象是 JavaScript 中的一个概念,它表示创建一个新对象时的一个模板,通过它可以继承另一个对象的所有属性。

基于原型的编程是一种面向对象的编程风格,其中行为重用(称为继承)是通过重用充当原型的现有对象的过程来执行的。该模型也可以称为原型、面向原型、无类或基于实例的编程。

设置对象原型

在 JavaScript 中,有多种设置对象原型的方法。

1. 使用 Object.create

Object.create() 方法创建一个新的对象,并允许你指定一个将被用作新对象原型的对象。

const personPrototype = {
  greet() {}
}

const carl = Object.create(personPrototype)
carl.greet()

这里我们创建了一个 personPrototype 对象,它有一个 greet() 方法。然后我们使用 Object.create() 来创建一个以 personPrototype 为原型的新对象。现在我们可以在新对象上调用 greet(),而原型提供了它的实现。

2. 使用构造函数

在 JavaScript 中,所有的函数都有一个名为 prototype 的属性。当你调用一个函数作为构造函数时,这个属性被设置为新构造对象的原型。因此,如果我们设置一个构造函数的 prototype,我们可以确保所有用该构造函数创建的对象都被赋予该原型。

const personPrototype = {
  greet() {}
}

function Person(name) {
  this.name = name
}

Object.assign(Person.prototype, personPrototype)

这里我们:创建了一个 personPrototype 对象,它具有 greet() 方法;创建了一个 Person() 构造函数,它初始化了要创建人物对象的名字。然后我们使用 Object.assignpersonPrototype 中定义的方法绑定到 Person 函数的 prototype 属性上。

函数的原型对象

每个函数就是一个对象,函数对象都有一个子对象原型(prototype)对象,类也是以函数的形式来定义。prototype 对象表示该函数的原型,也表示一个类的成员的集合。

先来看看原型对象都有什么?

function func () {}
console.log(func.prototype)

func.prototype 控制台显示: constructor: ƒ func()[[Prototype]]: Object,先看构造函数 constructor

在基于类、面向对象的编程中,构造函数(Constructor,缩写:ctor)是一种特殊类型的函数,被调用来创建对象。它准备新对象以供使用,通常接受构造函数用来设置所需成员变量的参数。

  • arguments: null:函数的参数集合。
  • caller: null:函数的调用者。
  • length: 1:函数的参数数量。
  • name: "func":函数的名称。
  • prototype: Object:函数的原型。
  • [[FunctionLocation]]: test.html:40:函数定义的位置。
  • [[Prototype]]: ƒ (): 函数的原型。
  • [[Prototype]]: Object:函数的原型。
  • [[Scopes]]: Scopes[1]:函数的作用域。

接着看 func.prototype 下的 prototype

  • constructor: ƒ func():构造函数。
  • [[Prototype]]: Object:对象的原型。

于是,陷入了无尽循环,因为函数的原型对象的构造函数就是函数自身。

原型链([[Prototype]]

原型链(Prototype chain)是 JavaScript 中对象的一个属性,它包含对象之间的链接,以便对象可以访问它们的原型链。原型链用于实现继承,它允许对象访问其原型链中的属性。

再来看看func.prototype[[Prototype]]

遵循 ECMAScript 标准,符号 someObject.[[Prototype]] 用于标识 someObject 的原型。内部插槽 [[Prototype]] 可以通过 Object.getPrototypeOf()Object.setPrototypeOf() 函数来访问。这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 __proto__ 访问器。为在保持简洁的同时避免混淆,在我们的符号中会避免使用 obj.__proto__,而是使用 obj.[[Prototype]] 作为代替。其对应于 Object.getPrototypeOf(obj)。它不应与函数的 func.prototype 属性混淆,后者指定在给定函数被用作构造函数时分配给所有对象实例的 [[Prototype]]

  • constructor: ƒ Object(): 该属性返回创建对象的函数,这里是 Object
  • hasOwnProperty: ƒ hasOwnProperty(): 该属性返回一个布尔值,表示某个对象是否具有指定的属性,且该属性在当前对象实例中而不是原型中。
  • isPrototypeOf: ƒ isPrototypeOf(): 该属性返回一个布尔值,表示指定的对象是否为当前对象的原型。
  • propertyIsEnumerable: ƒ propertyIsEnumerable(): 该属性返回一个布尔值,表示指定的属性是否可枚举。
  • toLocaleString: ƒ toLocaleString(): 该属性返回一个字符串,表示指定的对象。
  • toString: ƒ toString(): 该属性返回一个字符串,表示指定的对象。
  • valueOf: ƒ valueOf(): 该属性返回指定的对象。
  • __defineGetter__: ƒ __defineGetter__():该属性返回一个函数,用于为指定的属性添加一个 getter。
  • __defineSetter__: ƒ __defineSetter__():该属性返回一个函数,用于为指定的属性添加一个 setter。
  • __lookupGetter__: ƒ __lookupGetter__():该属性返回一个函数,用于获取指定属性的 getter。
  • __lookupSetter__: ƒ __lookupSetter__():该属性返回一个函数,用于获取指定属性的 setter。
  • __proto__: Object:该属性返回指定对象的原型对象。

还有 __proto__gettersetter 属性:

  • get __proto__: ƒ __proto__():该属性返回指定对象的原型对象。
  • set __proto__: ƒ __proto__():该属性设置指定对象的原型对象。

接着看 func.prototype.[[Prototype]]__proto__ 都有哪些属性。

  • constructor: ƒ Object(): 该属性返回创建对象的函数,这里是 Object
  • __proto__: null
  • 其余的属性与 func.prototype.[[Prototype]] 相同。

每个对象都有一个私有属性__proto__[[Prototype]])指向构造函数的 prototype 对象。原型对象也有一个自己的原型,层层向上直到一个对象的原型为 nullfunc.__proto__ 指向 ObjectObject.prototype.__proto__ 指向 null。根据定义,null 没有原型,并作为这个原型链(prototype chain)中的最后一个环节。

构造函数(constructor

原型的强大之处在于,如果一组属性应该出现在每一个实例上,那我们就可以重用它们——尤其是对于方法。

const boxes = [
  { value: 1, getValue() { return this.value } }
  { value: 2, getValue() { return this.value } }
]

这是不够好的,因为每一个实例都有自己的,做相同事情的函数属性,这是冗余且不必要的。

const boxPrototype = {
  getValue() { return this.value }
}

const boxes = [
  { value: 1, __proto__: boxPrototype }
  { value: 2, __proto__: boxPrototype }
]

这样,所有盒子的 getValue 方法都会引用相同的函数,降低了内存使用率。但是,手动绑定每个对象创建的 __proto__ 仍旧非常不方便。

function Box(value) {
  this.value = value
}

Box.prototype.getValue = function () {
  return this.value
}

const boxes = [ new Box(1), new Box(2) ]

我们就可以使用构造函数,它会自动为每个构造的对象设置 [[Prototype]]。构造函数是使用 new 调用的函数。

class Box {
  constructor(value) {
    this.value = value
  }

  getValue() {
    return this.value
  }
}

类是构造函数的语法糖,这意味着可以修改 Box.prototype 来改变所有实例的行为。

function Box(value) {
  this.value = value
}

Box.prototype.getValue = function () {
  return this.value
}

const box = new Box(1)

Box.prototype.getValue = function () {
  return this.value + 1
}

box.getValue()

重新赋值是一个不好的主意,原因有两点:

  1. 在重新赋值之前创建的实例的 [[Prototype]] 现在引用的是与重新赋值之后创建的实例的 [[Prototype]] 不同的对象——改变一个的 [[Prototype]] 不再改变另一个的 [[Prototype]]
  2. 除非你手动重新设置 constructor 属性,否则无法再通过 instance.constructor 追踪到构造函数,这可能会破坏用户期望的行为。一些内置操作也会读取 constructor 属性,如果没有设置,它们可能无法按预期工作。

字面量的隐式构造函数

JavaScript 中的一些字面量语法会创建隐式设置 [[Prototype]] 的实例。

// 对象字面量(没有 `__proto__` 键)自动将
// `Object.prototype` 作为它们的 `[[Prototype]]`
const object = { a: 1 }

// 数组字面量自动将 `Array.prototype` 作为它们的 `[[Prototype]]`
const array = [1, 2, 3]

// 正则表达式字面量自动将 `RegExp.prototype` 作为它们的 `[[Prototype]]`
const regexp = /abc/

我们可以将它们“解糖(de-sugar)”为构造函数形式。

const object = new Object({ a: 1 })
const array = new Array(1, 2, 3)
const regexp = new RegExp('abc')

有趣的是,由于历史原因,一些内置构造函数的 prototype 属性本身就是其自身的实例。例如,Number.prototype 是数字 0Array.prototype 是一个空数组,RegExp.prototype/(?:)/

构建更长的继承链

Constructor.prototype 属性将成为构造函数实例的 [[Prototype]],包括 Constructor.prototype 自身的 [[Prototype]]。默认情况下,Constructor.prototype 是一个普通对象, Object.getPrototypeOf(Constructor.prototype) === Object.prototype。唯一的例外是 Object.prototype 本身,Object.getPrototypeOf(Object.prototype) === null

function Constructor() {}

const obj = new Constructor()
// obj -> Constructor.prototype -> Object.prototype -> null

要构建更长的原型链,我们可用通过 Object.setPrototypeOf() 函数设置 Constructor.prototype[[Prototype]]

function Base() {}

function Derived() {}

// 将 `Derived.prototype` 的 `[[Prototype]]`
// 设置为 `Base.prototype`
Object.setPrototypeOf(Derived.prototype, Base.prototype)

const obj = new Derived()
// obj -> Derived.prototype -> Base.prototype -> Object.prototype -> null

在类的术语中,这等同于使用 extends 语法。

class Base {}
class Derived extends Base {}

const obj = new Derived();
// obj -> Derived.prototype -> Base.prototype -> Object.prototype -> null
展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
返回顶部
顶部