JavaScript 是动态的且没有静态类型,当谈到继承时,JavaScript 只有一种结构:对象。
在计算机科学中,对象(Object)可以是变量、数据结构、函数或方法。作为内存区域,对象包含一个值并由标识符引用。 在面向对象编程范式中,对象可以是变量、函数和数据结构的组合;特别是在基于类的范例变体中,对象指的是类的特定实例。 在数据库管理的关系模型中,对象可以是表或列,也可以是数据与数据库实体之间的关联(例如将人的年龄与特定人相关联)。
有这么一句话比较流行:“在 JavaScript 中,一切皆对象。”用“一切”来描述,这也太过绝对了吧?我的理解,这句话并不是说 JavaScript 中所有的东西都是对象,而是说 JavaScript 中的对象是 JavaScript 中的一个重要组成部分。
在 JavaScript 中,Number
、String
、Boolean
、BigInt
、Symbol
、Object
、function
等都可以按照对象的方式进行处理。null
和 undefined
以外原始类型都对应一个对象包装器。在一定条件下也会自动转为对象,也就是原始类型的“包装对象”,比如运算时隐式转换。换种方式可以理解为这些原始类型的字面量就是“包装对象”的实例,当然,instanceof`
并不这样认为。
除了 undefined
,几乎都是对象,可以操作属性和方法。不可否认,null
也是对象,只不过它是一个空对象,没有可操作的属性和方法。null
和 undefined
含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这是历史原因。
1995年 JavaScript 诞生时,最初像 Java 一样,只设置了 null
表示"无"。根据 C 语言的传统,null
可以自动转为 0
。但是,JavaScript 的设计者 Brendan Eich觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,如果 null
自动转为 0
,很不容易发现错误。
因此,Brendan Eich又设计了一个 undefined
。区别是这样的:null
是一个表示“空”的对象,转为数值时为 0
;undefined
是一个表示"此处无定义"的原始值,转为数值时为 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.assign
将 personPrototype
中定义的方法绑定到 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__
的 getter
和 setter
属性:
get __proto__: ƒ __proto__()
:该属性返回指定对象的原型对象。set __proto__: ƒ __proto__()
:该属性设置指定对象的原型对象。
接着看 func.prototype.[[Prototype]]
的 __proto__
都有哪些属性。
constructor: ƒ Object()
: 该属性返回创建对象的函数,这里是Object
。__proto__: null
- 其余的属性与
func.prototype.[[Prototype]]
相同。
每个对象都有一个私有属性__proto__
([[Prototype]]
)指向构造函数的 prototype
对象。原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null
。func.__proto__
指向 Object
,Object.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()
重新赋值是一个不好的主意,原因有两点:
- 在重新赋值之前创建的实例的
[[Prototype]]
现在引用的是与重新赋值之后创建的实例的[[Prototype]]
不同的对象——改变一个的[[Prototype]]
不再改变另一个的[[Prototype]]
。 - 除非你手动重新设置
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
是数字 0
,Array.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