认识对象
任何一个对象都是唯一的,这与它本身的状态无关
我们用状态来描述对象
状态的改变即是行为
面向对象三大特性
- 封装 -- 封装,复用,解耦,内聚(描述架构)
- 继承 -- Class Base 面向对象
- 多态 -- 描述动态性的程度
Class vs Prototype
Class(类)
- 类是一种常见的描述对象的方式
- “归类” 和 “分类” 两个主要流派
- 对于归类,多继承 C++
- 采用分类思想的计算机语言,单继承。并且会有一个基类 Object
Prototype(原型)
- 原型是一种更接近人类原始认知的描述对象的方式
- 我们并不试图做严谨的分类,而是采用 “相似” 这样的方法来描述对象
- 任何对象仅仅需要描述它自己与原型的区别即可
JavaScript 对象
对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值
可以把 ECMAScript 的对象想象成一张散列表,其中的内容就是一组 名/值 对,值可以是数据或者函数
属性类型
ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义。因此,开发者不能在 JavaScript 中直接访问这些特性
属性分两种:数据属性和访问器属性
数据属性
数据属性有 4 个特性描述它们的行为
[[Configurable]]
:表示属性是否可以通过delete
删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true
[[Enumerable]]
:表示属性是否可以通过for-in
循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true
[[Writable]]
:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是true
[[value]]
:包含属性实际的值
let person = {
name: 'andy'
}
在上面例子中,会将属性显式添加到对象之后,[[Configurable]]
、[[Enumerable]]
和 [[Writable]]
都会被设置为 true
,而 [[Value]]
特性会被设置为指定的值
这里,我们创建了一个名为 name
的属性的对象,并赋值 'andy'
。这意味着 [[value]]
特性会被设置为 'andy'
,之后对这个值的任何修改都会保存到这个位置
如果想要修改属性的默认特性,必须使用 Object.defineProperty(obj, prop, descriptor)
方法
这个方法接收三个参数
obj
:要定义属性的对象prop
:要定义或修改的属性的名称或Symbol
descriptor
:要定义或修改的属性描述符
let person = {}
Object.defineProperty(person, 'name', {
writable: false,
configurable: false,
enumerable: false,
value: 'andy'
})
注意:
- 当设置
writable
为false
,这个属性的值就不能再被修改。当尝试修改这个属性时,在非严格模式下会被忽略,在严格模式下会抛出错误 - 当设置
configurable
为false
,这个属性就不能从对象上删除。当尝试对这个属性调用delete
删除,在非严格模式下会被忽略,在严格模式下会抛出错误。 - 当一个属性被定义为不可配置之后,就不能再变回可配置,再次调用
Object.defineProperty()
并修改任何非writable
属性会导致错误 - 在调用
Object.defineProperty()
时,configurable
、enumerable
和writable
的值如果不指定,都默认为false
访问器属性
访问器属性有 4 个特性描述它们的行为
[[Configurable]]
:表示属性是否可以通过delete
删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true
[[Enumerable]]
:表示属性是否可以通过for-in
循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true
[[Get]]
:获取函数,在读取属性时调用。默认值为undefined
[[Set]]
:设置函数,在读取属性时调用。默认值为undefined
访问器属性是不能直接定义的,必须使用 Object.defineProperty()
let perosn = {
_age: 18
}
Object.defineProperty(person, 'age', {
get() {
return this._age
}
set(newValue) {
if(newValue > 18) {
this._age = newValue
}
}
})
定义多个属性
接收两个参数:
obj
:要定义或修改属性的对象props
:要定义其可枚举属性或修改的属性描述符的对象
读取属性的特性
接收两个参数:
obj
: 需要查找的目标对象prop
:目标对象内属性名称
返回值:
如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined
接收一个参数:
obj
:任意对象
返回值:
所指定对象的所有自有属性的描述符,如果没有任何自有属性,则返回空对象
这个方法实际上会在每个自有属性上调用 Object.getOwnPropertyDescriptor()
并在一个新对象中返回它们
Object 的一些扩展
Object.assign
参数:
target
:目标对象sources
:源对象
返回值:
目标对象
注意:
如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖;后面的源对象的属性将类似地覆盖前面的源对象的属性
Object.assign
方法只会拷贝源对象自有(Object.hasOwnProperty
返回 true
)的并且可枚举(Object.propertyIsEnumbeable
返回 true
)的属性到目标对象
Stirng
类型和 Symbol
类型的属性会被拷贝
这个方法会使用源对象上的 [[Get]]
取得属性的值,然后使用目标对象上的 [[Set]]
设置属性的值
Object.assign()
实际上对每个源对象执行的是浅拷贝
如果赋值期间出错,则操作会终止并退出,同时抛出错误;Object.assign()
没有 “回滚” 之前赋值的概念,因此它是一个尽力而为、可能只会完成部分拷贝的方法
let dest = {}
let src = { a: 'foo' }
let result = Object.assign(dest, src)
console.log(dest === result) // true
console.log(dest !== src) // true
console.log(result) // {a: "foo"}
Object.is
Object.is(+0, 0) // true
Object.is(-0, 0) // false
Object.is(NaN, NaN) // true
// Polyfill
if (!Object.is) {
Object.is = function (x, y) {
if (x === y) {
return x !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
}
创建对象
可以使用 Object
构造函数或对象字面量方便地创建对象,但是也有明显的不足:创建具有同样接口的多个对象需要重复编写很多代码
ES6 之前没有正式支持面向对象的结构,比如类和继承。我们可以运用原型继承来模拟同样的行为(不推荐)
ES6 开始正式支持类和继承。ES6 的类旨在完全涵盖之前规范设计的基于原型的继承模式。不过,无论从哪方面看,ES6 的类都仅仅是封装了 ES5.1 构造函数加原型继承的语法糖而已
工厂模式
用于抽象创建特定对象的过程
?:
function createPerson(name, age) {
let o = new Object()
o.name = name
o.age = age
o.sayName = function () {
console.log(this.name)
}
return o
}
let person = createPerson('Andy', 18)
虽然解决了创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)
构造函数模式
ECMAScript 中的构造函数是用于创建特定类型对象的
?:
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
let person = new Person('Andy', 18)
这个例子中, Person()
构造函数代替了 createPerson()
工厂函数。实际上,内部代码基本一致,有如下区别:
- 没有显示地创建对象
- 属性和方法直接赋值给了
this
- 没有
return
要创建 Person
的实例,应使用 new
操作符。以这种方式调用构造函数会执行如下操作:
- 在内存中创建一个新对象
- 这个新对象内部的
[[Prototype]]
特性被赋值为构造函数的prototype
属性 - 构造函数内部的
this
被赋值为这个新对象(即this
指向新对象) - 指向构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
构造函数不一定写成函数声明的形式;赋值给变量的函数表达式也可以:
let Person = function(name, age) {
// ...
}
在实例化时,如果不传参数,那么构造函数后面的括弧可不加。只要有 new
操作符,就可以调用相应的构造函数:
function Person() {
this.name = 'Andy'
this.sayName = function() {
console.log(this.name)
}
}
let p1 = new Person()
let p2 = new Person
1. 构造函数也是函数
构造函数与普通函数唯一的区别就是调用方式不同
任何函数只用使用 new
操作符调用的构造函数,而不是用 new
操作符调用的函数就是普通函数
2. 构造函数的问题
其定义的方法会在每个实例上都创建一遍
在上面的例子中,p1
和 p2
都有名为 sayName()
的方法,但这两个方法不是同一个 Function
实例
在 ECMAScript 中的函数时对象,因此每次定义函数时,都会初始化一个对象
function Person() {
this.name = 'Andy'
this.sayName = new Function('console.log(this.name)') // 逻辑等价
}
console.log(p1.sayName == p2.sayName) // false
都是做一样的事,没必要定义两个不同的 Function
实例。且 this
对象可以把函数与对象的绑定推迟到运行时
要解决这个问题,可以把函数定义转移到构造函数外部:
function Person() {
this.name = 'Andy'
this.sayName = sayName
}
function sayName() {
console.log(this.name)
}
这样,p1
和 p2
共享了定义在全局作用域上的 sayName()
函数。虽然解决了相同逻辑的函数重复定义问题,但是全局作用域也因此被搞乱。如果需要多个方法,那么就要在全局作用域定义多个函数。会导致自定义类型引用的代码不能很好地聚集在一起。这个问题可以通过原型模式来解决
原型模式
实际上,这个对象就是通过调用构造函数创建的对象的原型
function Person() { }
Person.prototype.name = 'Andy'
Person.prototype.sayName = function () {
console.log(this.name)
}
let p1 = new Person
p1.sayName() // "Andy"
let p2 = new Person()
p2.sayName() // "Andy"
console.log(p1.sayName === p2.sayName) // true
1. 理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype
属性(指向原型对象)
默认情况下,所有原型对象自动获得一个名为 constructor
的属性,指回与之关联的构造函数
可以给原型对象添加其他属性和方法
Person.prototype.constructor === Person // true
在自定义构造函数时,原型对象默认只会获得 constructor
属性,其他的所有方法都继承自 Object
每次调用构造函数创建一个新实例,这个实例的内部 [[prototype]]
指针就会被赋值为构造函数的原型对象
脚本中没有访问这个 [[prototype]]
特性的标准方式,Firfox、Safari 和 Chrome 会在每个对象上暴露 __proto__
属性,通过这个属性可以访问对象的原型
关键点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有
/**
* 构造函数可以是函数表达式,也可以是函数声明
* function Person() {}
* let Person = function() {}
*/
function Person() { }
/**
* 声明之后,构造函数就有了一个与之关联的原型对象
*/
console.log(typeof Person.prototype)
console.log(Person.prototype)
// {
// constructor: f Person(),
// __proto__: Object
// }
/**
* 构造函数有一个 prototype 属性引用其原型对象
* 这个原型对象也有一个 constructor 属性,引用这个构造函数
* 两者循环引用
*/
console.log(Person.prototype.constructor === Person) // true
/**
* 正常的原型链都会终止于 Object 的原型对象
* Object 原型的原型是 null
*/
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Person.prototype.__proto__.constructor === Object) // true
console.log(Person.prototype.__proto__.__proto__ === null) // true
let person1 = new Person
let person2 = new Person()
/**
* 实例通过 __proto__ 链接到原型对象,它实际上指向隐藏特性 [[Prototype]]
*
* 构造函数通过 prototype 链接到原型对象
*
* 实例与构造函数没有直接关系,与原型对象有直接联系
*/
console.log(person1.__proto__ === Person.prototype) // true
console.log(person1.__proto__.constructor === Person) // true
/**
* 同一个构造函数创建的实例,共享同一个原型对象
*/
console.log(person1.__proto__ === person2.__proto__) // true
/**
* instanceof 检查实例的原型链中是否包含指定构造函数的原型
*/
console.log(person1 instanceof Person) // true
console.log(person1 instanceof Object) // true
console.log(Person.prototype instanceof Object) // true
虽然不是所有实现都对外暴露了 [[Prototype]]
,但可以使用 isPrototypeOf()
方法确定两个对象之间的这种关系。本质上,isPrototype()
会比较传入参数的 [[Prototype]]
是否指向调用它的对象
Person.prototype.isPrototypeOf(person1) // true
Person.prototype.isPrototypeOf(person2) // true
ECMAScript 的 Object
类型有一个方法 Object.getPrototypeOf()
,返回参数的内部特性 [[Prototype]]
值
Object.getPrototypeOf(person1) === Person.prototype // true
Object
类型还有一个 setPrototypeOf()
方法,可以向实例的私有特性 [[Prototype]]
写入一个新值。这样就可以重写一个对象的原型继承关系:
let biped = {
numLegs: 2
}
let person = {
name: 'Matt'
}
Object.setPrototypeOf(person, biped)
console.log(person.numLegs) // 2
console.log(Object.getPrototypeOf(person) === biped) // true
注意:Object.setPrototypeOf()
可能会严重影响代码性能。Mozilla 文档中指出:
为了避免使用 Object.setPrototypeOf()
可能造成的性能下降,可以通过 Object.create()
来创建一个指定原型的新对象:
let biped = {
numLegs: 2
}
let person = Object.create(biped)
console.log(person.numLegs) // 2
console.log(Object.getPrototypeOf(person) === biped) // true
2. 原型层级
读取实例的属性时,首先会在实例上搜索这个属性;如果没有找到,则会继承搜索实例的原型
实例可以读取原型对象上的值
实例不可重写这些值
如果在实例上添加一个与原型对象中同名的属性,会在实例上创建这个属性,这个属性会遮蔽(shadow)原型对象上的属性
通过 delete
操作服可以完全删除实例上的属性,从而让标识符解析过程能够继续搜索原型对象
function Person() {}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 18
Person.prototype.sayName = function() {
console.log(this.name)
}
let person1 = new Person
let person2 = new Person
person1.name = 'Andy'
console.log(person1.name) // "Andy"
console.log(person2.name) // "Nicholas"
delete person1.name
console.log(person1.name) // "Nicholas"
hasOwnProperty()
方法用于确定某个属性是在实例上还是在原型对象上
这个方法是继承自 Object
,会在属性存在于调用它的对象实例上时返回 true
function Person() {}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 18
Person.prototype.sayName = function() {
console.log(this.name)
}
let person1 = new Person
let person2 = new Person
console.log(person1.hasOwnProperty('name')) // false
person1.name = 'Andy'
console.log(person1.name) // "Andy"
console.log(person1.hasOwnProperty('name')) // true
console.log(person2.name) // "Nicholas"
console.log(person2.hasOwnProperty('name')) // false
delete person1.name
console.log(person1.name) // "Nicholas"
console.log(person1.hasOwnProperty('name')) // false
3. 原型和 in 操作符
in
操作符有两种使用方式:单独使用和 for-in
循环
在单独使用时,in
操作符会在可以通过对象访问指定属性时返回 true
,无论该属性在实例上还是在原型上
function Person() {}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 18
Person.prototype.sayName = function() {
console.log(this.name)
}
let person1 = new Person
let person2 = new Person
console.log(person1.hasOwnProperty('name')) // false
console.log('name' in person1) // true
person1.name = 'Andy'
console.log(person1.name) // "Andy"
console.log(person1.hasOwnProperty('name')) // true
console.log('name' in person1) // true
console.log(person2.name) // "Nicholas"
console.log(person2.hasOwnProperty('name')) // false
console.log('name' in person2) // true
delete person1.name
console.log(person1.name) // "Nicholas"
console.log(person1.hasOwnProperty('name')) // false
console.log('name' in person1) // true
如果要判断某个属性是否存在于原型上?
function hasPrototypeProperty(obj, key) {
return !obj.hasOwnProperty(key) && (key in obj)
}
只要对象可以访问, in
操作符就返回 true
,而 hasOwnProperty()
只有属性存在于实例上时才返回 true
。因此,只要 in
操作符返回 true
且 hasOwnProperty()
返回 false
,就说明该属性是一个原型属性
在 for-in
循环中使用 in
操作符,可以通过对象访问且可以被枚举([[Enumerable]]
特性为 true
)的属性都会返回,包括实例属性和原型属性
可以通过 Object.keys()
方法获取对象上所有可枚举的实例属性
可以通过 Object.getOwnPropertyNames()
方法获取所有实例属性
function Person() {}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 18
Person.prototype.sayName = function() {
console.log(this.name)
}
let keys = Object.keys(Person.prototype)
console.log(keys) // ["name", "age", "sayName"]
let p1 = new Person
p1.name = "Andy"
p1.age = 20
let p1keys = Object.keys(p1)
console.log(p1keys) // ["name", "age"]
let allKeys = Object.getOwnPropertyNames(Person.prototype)
console.log(allKeys) // ["constructor", "name", "age", "sayName"]
在 ECMAScript6 新增符号(Symbol
)类型后,相应地增加了 Object.getOwnPropertySymbols()
方法
let k1 = Symbol('k1')
let k2 = Symbol('k2')
let o = {
[k1]: 'k1',
[k2]: 'k2',
}
console.log(Object.getOwnPropertySymbols(o)) // [Symbol(k1), Symbol(k2)]
4. 属性枚举顺序
for-in
和 Object,keys()
的枚举顺序是不确定的,取决于 JavaScript 引擎
Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
和 Object.assign()
的枚举顺序是确定的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键
5. 原型相关语法
ECMAScript 2017 新增两个静态方法,用于将对象内容转换为序列化的 -- 更重要的是可迭代的 -- 格式
Object.values()
返回对象值的数组
Object.entries
返回键/值对数组
const obj = {
foo: 'bar',
baz: 2,
qux: {}
}
console.log(Object.values(obj)) // ["bar", 2, {}]
console.log(Object.entries(obj)) // [["foo", "bar"], ["baz", 2], ["qux", {}]]
非字符串属性会被转换为字符串输出。
这两个方法执行对象的浅复制
const obj = {
qux: {}
}
console.log(Object.values(obj)[0] === obj.qux) // true
console.log(Object.entries(obj)[0][1] === obj.qux) // true
符号属性会被忽略
const k = Symbol()
const obj = {
[k]: 'foo'
}
console.log(Object.values(obj)) // []
console.log(Object.entries(obj)) // []
6. 原型的动态性
因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改都会在实例上反映出来
实例和原型之间的链接就是简单的指针,而不是保存的副本
重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型
实例只有指向原型的指针,没有指向构造函数的指针
function Person() {}
let p = new Person()
Person.prototype = {
constructor: Person,
name: 'Andy',
age: 18,
sayName() {
console.log(this.name)
}
}
p.sayName() // Uncaught TypeError: p.sayName is not a function
重写构造函数上的原型之后再创建的实例才会引用新的原型。而在此之前创建的实例仍然会引用最初的原型
7. 原生对象原型
所有原生引用类型的构造函数(包括 Object
、Array
、 String
等)都在原型上定义了实例方法
通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。可以像修改自定义对象原型一样修改原生对象原型
注意:尽管可以这么做,但并不推荐再生产环境中修改原生对象原型。这样做很可能造成误会,而且可能引发命名冲突。还有可能意外重写原生的方法。推荐的做法是创建一个自定义的类,继承原生类型
8. 原型的问题
弱化了向构造函数传递初始化参数的能力,会导致所有实例默认取得相同的属性值。虽然这会带来不便,但这不是原型最大的问题。原型最主要的问题源自它的共享特性。
原型上的所有属性是在实例间共享,这对函数来说比较合适。真正的问题来自包含引用值的属性
function Person() {}
Person.prototype = {
constructor: Person,
name: 'Andy',
age: 18,
friends: ['Lyn', 'Mike'],
sayName() {
console.log(this.name)
}
}
let person1 = new Person()
let person2 = new Person()
person1.friends.push('Tony')
console.log(person1.friends) // ["Lyn", "Mike", "Tony"]
console.log(person2.friends) // ["Lyn", "Mike", "Tony"]
console.log(person1.friends === person2.friends) // true
手写实现 new
function newFunc(...args) {
// 取出 args 数组的第一个参数,即目标构造函数
const constructor = args.shift()
// 创建一个空对象,并使这个对象继承构造函数的 prototype 属性
// obj.__proto__ === constructor.prototype
const obj = Object.create(constructor.prototype)
// 执行构造函数,得到构造函数返回结果
// 这里使用 apply 使构造函数内的 this 指向 obj
const result = constructor.apply(obj, args)
// 如果构造函数执行后,返回的结果是非空对象,则直接返回该结果,否则返回 obj
return (typeof result === 'object' && result !== null) ? result : obj
}
继承
原型链
构造函数、原型和实例的关系:
-
每个构造函数都有一个原型对象,原型有一个属性指回构造函数
-
实例有一个内部指针指向原型
如果原型是另一个类型的实例呢?
那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就走实例和原型之间构造了一条原型链
?:
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function() {
return this.property
}
function SubType() {
this.subproperty = false
}
// 继承 SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
let instance = new SubType()
console.log(instance.getSuperValue()) // true
原型链扩展了原型搜索机制。我们知道,在读取实例上的属性时,首先会在实例上搜索这个属性;如果没找到,则会继承搜索实例的原型;在通过原型链实现继承之后,搜索就可以继承向上,搜索原型的原型。在这个例子中,调用 instance.getSuperValue()
经过了 3 步搜索:instance
、 SubType.prototype
、 SuperType.prototype
,最后一步才找到这个方法
1. 默认原型
默认情况下,所以引用类型都继承自 Object
,这也是通过原型链实现的。
任何函数的默认原型都是 Object
的一个实例,这意味着这个实例有一个内部指针指向 Object.prototype
2. 原型与实例的关系
原型与实例的关系可以通过两种方式来确定
instanceof
操作符:如果一个实例的原型链中出现过相应的构造函数,则instanceof
返回true
instance instanceof Object // true
instance instanceof SuperType // true
instance instanceof SubType // true
isPrototypeOf()
方法:原型链中的每个原型都可以调用这个方法,只有原型链中包含这个原型,就返回true
Object.prototype.isPrototypeOf(instance) // true
SuperType.prototype.isPrototypeOf(instance) // true
SubType.prototype.isPrototypeOf(instance) // true
- 以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function() {
return this.property
}
function SubType() {
this.subproperty = false
}
// 继承 SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
SubType.prototype = {
getSubValue() {
return this.subproperty;
},
someOtherMethod() {
return false;
}
}
let instance = new SubType()
console.log(instance.getSuperValue()) // Uncaught TypeError: instance.getSuperValue is not a function
3. 原型链的问题
- 主要问题出现在原型中包含引用值的时候,原型中包含的引用值会在所有实例间共享
function SuperType() {
this.colors = ['red', 'green', 'blue']
}
function SubType() {}
SubType.prototype = new SuperType()
let instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "green", "blue", "black"]
let instance2 = new SubType()
console.log(instance2.colors) // ["red", "green", "blue", "black"]
- 子类型在实例化时不能给父类型的构造函数传参
经典继承(盗用构造函数)
在子类构造函数中调用父类构造函数
函数就是在特定上下文中执行代码的简单对象,所以可以使用 apply()
和 call()
方法以新创建的对象上下文执行构造函数
function SuperType() {
this.colors = ['red', 'green', 'blue']
}
function SubType() {
// 继承 SuperType
SuperType.call(this)
}
let instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "green", "blue", "black"]
let instance2 = new SubType()
console.log(instance2.colors) // ["red", "green", "blue"]
1. 传递参数
function SuperType(name) {
this.name = name
}
function SubType() {
SuperType.call(this, 'Andy')
this.age = 18
}
let instance = new SubType
console.log(instance.name) // "Andy"
console.log(instance.age) // 18
2. 经典继承的问题
- 必须在构造函数中定义方法,因此函数不能重用
- 子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式
组合继承
综合了原型链和经典继承:使用原型链继承原型上的属性和方法,用盗用构造函数继承实例属性。
这样即可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function SuperType(name) {
this.name = name,
this.colors = ['red', 'green', 'blue']
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name)
this.age = age
}
// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function() {
console.log(this.age)
}
let instance1 = new SubType('Nicholas', 20)
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "green", "blue", "black"]
instance1.sayName() // Nicholas
instance1.sayAge() // 20
let instance2 = new SubType('Andy', 18)
console.log(instance2.colors) // ["red", "green", "blue"]
instance2.sayName() // Andy
instance2.sayAge() // 18
组合继承弥补了原型链和经典继承的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof
操作服和 isPrototypeOf()
方法识别合成对象的能力
原型式继承
function object(o) {
function F() {}
F.prototype = o
return new F()
}
object
方法本质上是对传入对象执行了一次浅复制
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
let person1 = object(person)
person1.name = 'Greg'
person1.friends.push('Rob')
let person2 = object(person)
person2.name = 'Linda'
person2.friends.push('Barbie')
console.log(person.friends) // ["Shelby", "Court", "Van", "Rob", "Barbie"]
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
let person1 = Object.create(person, {
name: {
value: 'Greg'
}
})
console.log(person1.name) // Greg
注意:属性中包含的引用值始终会在相关对象间共享,和使用原型模式是一样的
寄生式继承
背后思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function createAnother(original) {
// let clone = object(original)
let clone = Object.create(original)
clone.sayHi = function() {
console.log('hi')
}
return clone
}
object()
函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用
注意:通过寄生式继承给对象添加函数会导致函数难以重用
寄生式组合继承
组合继承存在效率问题:父类构造函数始终会被调用两次;一次是在创建子类原型时调用,另一次是在子类型构造函数中调用。
function SuperType(name) {
this.name = name
this.colors = ['red', 'green', 'blue']
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name) // 第二次调用 SuperType()
this.age = age
}
SubType.prototype = new SuperType() // 第一次调用 SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function() {
console.log(this.age)
}
寄生式组合继承通过盗用构造函数继承属性,使用混合式原型链继承方法
function inheritPrototype(subType, superType) {
let prototype = Object.create(superType.prototype)
prototype.constructor = subType
subType.prototype = prototype
}
inheritPrototype()
函数实现了寄生式继承的核心逻辑。
这个函数接收两个参数:子类构造函数和父类构造函数;
第一步创建父类原型的一个副本
给返回的 prototype
对象设置 constructor
属性,解决由于重写原型导致默认 constructor
丢失问题
最后将新创建的对象赋值给子类型的原型
改下上面组合继承:
function SuperType(name) {
this.name = name
this.colors = ['red', 'green', 'blue']
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function() {
console.log(this.age)
}
类
使用 ECMAScript 5 的特性来模拟类似于类(class-like)的行为都有自己的问题
ECMAScript 6 引入 class
关键字具有正式定义类的能力
类(class
)是 ECMAScript 中新的基础性语法糖,表面上看起来可以支持正式的类继承,但实际上它背后使用的仍然是原型和构造函数的概念
类定义
类声明和类表达式
// 类声明
class Animal {}
// 类表达式
const Animal = class {}
与函数的不同:
- 类定义不能提升
- 函数受函数作用域限制,类受块作用域限制
类的构成
- 构造函数方法
- 实例方法
- 获取函数
- 设置函数
- 静态类方法
这些都不是必须的;类定义中的代码都在严格模式下执行
// 空类定义
class Foo {}
// 有构造函数
class Bar {
constructor() {}
}
// 有获取函数
class Baz {
get myBaz() {}
}
// 有静态方法
class Qux {
static myQux() {}
}
类表达式的名称是可选的
可以通过 name
属性取得类表达式的名称字符串
不能在类表达式作用域外部访问这个标识符
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name)
}
}
let p = new Person()
p.identify() // PersonName PersonName
console.log(Person.name) // PersonName
console.log(PersonName) // Uncaught ReferenceError: PersonName is not defined
类构造函数
constructor
关键字表示类的构造函数;会告诉解释器在使用 new
操作符创建类的新实例时,应该调用这个函数
1. 实例化
使用 new
调用类的构造函数会执行如下操作:
- 在内存中创建一个新对象
- 这个新对象内部的
[[Prototype]]
特性被赋值为构造函数的prototype
属性 - 构造函数内部的
this
被赋值为这个新对象(即this
指向新对象) - 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回这个对象,否则返回之前创建的新对象
与构造函数的区别:
调用类构造函数必须使用 new
操作符;普通构造函数如果不使用 new
调用,那么会以全局的 this
作为内部对象;调用类构造函数没有使用 new
则会抛出错误
实例、原型和类成员
1. 实例成员
添加到新创建实例(this
)上的“自有”属性
每个实例都对应一个唯一的成员对象
2. 原型方法与访问器
类块中定义的方法
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance')
}
locate() {
// 定义在类的原型对象上
console.log('prototype')
}
}
可以把方法定义在类构造函数中或者类块中,原始值或对象不可以
类定义支持获取和设置访问器
class Person {
set name(newName) {
this._name = newName
}
get name() {
return this._name
}
}
3. 静态类方法
使用 static
关键字
静态成员每个类只能有一个
在静态成员中,this
引用类自身
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance')
}
locate() {
// 定义在类的原型对象上
console.log('prototype')
}
// 定义在类本身上
static locate() {
console.log('class', this)
}
}
静态类方法非常适合作为实例工厂:
class Person {
constructor(name) {
this._name = name
}
sayName() {
console.log(this._name)
}
static create() {
return new Person('Andy')
}
}
console.log(Person.create())
4. 非函数原型和类成员
类定义不支持在原型或类上添加成员数据,但在类定义外部,可以手动添加
5. 迭代器与发生器方法
类定义语法支持在原型和类本身上定义生成器方法
继承
1. 基础继承
使用 extends
关键字,就可以继承任何拥有 [[Construct]]
和原型的对象
class Vehicle {
identifyPrototype(id) {
console.log(id, this)
}
static identifyClass(id) {
console.log(id, this)
}
}
class Bus extends Vehicle {}
let v = new Vehicle()
let b = new Bus()
b.identifyPrototype('bus') // bus Bus {}
v.identifyPrototype('vehicle') // vehicle Vehicle {}
Bus.identifyClass('bus') // bus class Bus extends Vehicle {}
Vehicle.identifyClass('vehicle') // vehicle class Vehicle {}
extends
关键字也支持类表达式 let Bus = class extends Vehicle {}
2. 构造函数、HomeObject 和 super()
super()
派生类的方法可以通过 super
关键字引用它们的原型。
在类构造函数中使用 super
可以调用父类构造函数
class Vehicle {
constructor() {
this.hasEngine = true
}
}
class Bus extends Vehicle {
constructor() {
// 不要在调用 super() 之前引用 this,否则会抛出 ReferenceError
super() // 相当于 super.constructor()
console.log(this instanceof Vehicle) // true
console.log(this) // Bus { hasEngine: true }
}
}
new Bus()
在静态方法中可以通过 super 调用继承的类上定义的静态方法:
class Vehicle {
static identify() {
console.log('vehicle')
}
}
class Bus extends Vehicle {
static identify() {
super.identify()
}
}
Bus.identify() // vehicle
在使用 super
时要注意的几个问题:
super
只能在派生类的构造函数和静态方法中使用
class Vehicle {
constructor() {
super() // Uncaught SyntaxError: 'super' keyword unexpected here
}
}
- 不能单独应用
super
关键字
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(super) // Uncaught SyntaxError: 'super' keyword unexpected here
}
}
- 调用
super
会调用父类构造函数,并将返回的实例赋值给this
class Vehicle {}
class Bus extends Vehicle {
constructor() {
super()
console.log(this instanceof Vehicle)
}
}
new Bus() // true
super()
的行为如同调用构造函数,如果需要给父类构造函数传参,需手动传入
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate
}
}
class Bus extends Vehicle {
constructor(licensePlate) {
super(licensePlate)
}
}
console.log(new Bus('A2333')) // Bus {licensePlate: "A2333"}
- 如果没有定义类构造函数,在实例化派生类时会调用
super()
,而且会传入所有传给派生类的参数
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate
}
}
class Bus extends Vehicle {}
console.log(new Bus('A2333')) // Bus {licensePlate: "A2333"}
- 在类构造函数中,不能在调用
super()
之前引用this
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(this)
}
}
new Bus() // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
- 如果在派生类中显示定义类构造函数,则要么必须在其中调用
super()
,要么必须在其中返回一个对象
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super()
}
}
class Van extends Vehicle {
constructor() {
return {}
}
}
console.log(new Car()) // Car {}
console.log(new Bus()) // Bus {}
console.log(new Van()) // {}
3. 抽象基类
ECMAScript 没有专门支持抽象基类的语法,可以通过实例化时检测 new.target
(保存通过 new
关键字调用的类或函数)是不是抽象基类,可以阻止对抽象基类的实例化:
class Vehicle {
constructor() {
console.log(new.target)
if(new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated')
}
}
}
class Bus extends Vehicle {}
new Bus() // class Bus extends Vehicle {}
new Vehicle() // class Vehicle()
// Uncaught Error: Vehicle cannot be directly instantiated
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!