前言
本文按照JS红宝书第八章的顺序整理。看完这篇文章,你将能回答如下的问题:
- JS 创建对象有几种方式【8.1.1 节】
- Object.defineProperty的作用?他有几个数据属性,这几个属性的区别是什么?他有几个访问器属性,区别是什么?【8.1.1 节】
- Object.defindProperties 作用?【8.1.2 节】
- 对象属性描述符是什么,有什么办法能够获取到对象属性的描述符?【8.1.3】
- JS 中合并对象有什么方式? Object.assign 的作用?【8.1.4 节】注意深浅拷贝的知识点
- 有什么办法能够正确比较两个空{},也就是{} === {}是 true 还是 false?NaN === NaN 是 true 吗?对 Object.is()方法的理解
- ES6 中,对象增加了哪些语法?【8.1.6】
- 创建对象有几种模式/方式?【8.2】
- 创建对象的工厂模式是什么?【8.2.2】
- 构造函数模式创建对象和工厂模式有什么区别?【8.2.3】
- 构造函数和普通函数有什么区别? new 关键字干了什么?构造函数有什么问题?
- 谈谈对 JS 原型的理解,原型三角关系【8.2.4】
- 你有哪些方法判断构造函数是否在实例对象的原型链上?instanceof 的作用? isPrototypeOf 方法是怎么用的?【8.2.4】
- 描述 JS 原型链的查找方式?【8.2.4】
- 怎么样判断某个属性是自己身上的?
- Object.getOwnPropertyDescriptors()的作用?【8.2.4】 for-in、Object.keys()、Object.getOwnProperty() 和Object.getOwnSymbolProperty()的区别,此外,他们枚举的属性是有序的吗?【8.2.4】
- Object.prototype.proto 指向谁?【8.3.1】
- 原型链继承存在什么问题【8.3.1】
- 盗用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承的方式描述?有什么优点和缺点【8.3.3~8.3.6】
- 定义类有几种方式,他们有什么区别?【8.4.1】
- 类构造函数和普通构造函数有什么区别?【8.4.2】
- 你知道类的 static 关键字有什么作用吗?【8.4.3】
- 类如何实现继承,extends 关键字的理解,super 关键字的理解【8.4.4】
- 知道抽象基类的概念吗?【8.4.4】
- 知道类混入的概念吗?【8.4.4】
8.1 理解对象
对象:就是一组没有特定顺序的值
构造函数-早期创建对象的方法
let person = new Object()
person.name = 'Nicholas';
person.age = 29
person.job = 'Software Engineer';
person.sayName = function () {
console.log(this.name, 'this.name');
}
字面量创建
let person2 = {
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function () {
console.log(this.name, 'this.name');
}
}
person2.sayName()
注意 Object.create()也能返回一个新对象,这个方法下文会介绍
8.1.1 属性的类型 Object.defineProperty
JS 使用内部特性描述属性的特征,开发者不能直接访问这些特性。如果每个特性是内部特性,则其是 [[]] 这样两个中括号,如 [[Enumerable]]
属性分为:数据属性、访问器属性
数据属性
| 属性 | 作用 |
|---|---|
| [[Configurable]] | 1. 属性是否可以通过 delete 删除,并重新定义 |
- 是否可以修改他的特性
- 是否可以把他改为访问器属性
- 直接定义在对象身上的属性默认为 true。通过 Object.define 定义的,默认为 false | | [[Enumberable]] | 1. 是否可以通过 for in 进行遍历返回
- 默认也是 true,通过 Object.define 定义的默认为 false | | [[Writable]] | 1. 属性值是否可以被修改
- 默认和上面两个一致 | | [[Value]] | 1. 属性实际值
- 默认为 undefined |
如下,person 的 name 属性,值通过 value 定义,writable 为 false,改为 xhg2 并不能生效
let person = new Object()
Object.defineProperty(person, 'name', {
value: 'xhg',
writable: false,
})
console.log(person.name, 'person.name');
person.name = 'xhg2'
console.log(person.name, 'person.name');
其他的 enumerable 和 configurable 都是默认 false,所以尝试删除,并不能生效
delete person.name
console.log(person, 'person');
若把configurable 主动改为 true, 就可以删除了:
let person2 = new Object()
Object.defineProperty(person2, 'name', {
value: 'xhg',
writable: false,
configurable: true
})
delete person2.name
console.log(person2, 'person');
configurable 若主动改为 false 后,再尝试改为 true,则会直接报错
let person3 = new Object()
Object.defineProperty(person3, 'name', {
value: 'xhg',
writable: false,
configurable: false
})
Object.defineProperty(person3, 'name', {
value: 'xhg',
writable: false,
configurable: true
})
delete person3.name
调用 Object.defineProperty(),configurable 、enumerable、writable 的值如果不指定,则默认都为 false
访问器属性
不包含数据值,包含 [[GET]] 和 [[SET]] 函数。读取对象属性值,调用 [[GET]] 获取值;修改对象属性盒子,调用 [[SET]] 来获取值
| 属性 | 作用 |
|---|---|
| [[Configurable]] | 1. 属性是否可以通过 delete 删除,并重新定义 |
- 是否可以修改他的特性
- 是否可以把他改为数据属性
- 直接定义在对象身上的属性默认为 true。通过 Object.define 定义的,默认为 false | | [[Enumerable]] | 1. 是否可以通过 for in 进行遍历返回
- 默认也是 true,通过 Object.define 定义的默认为 false | | [[GET]] | 1. 获取函数,读取属性时调用,默认值为 undefined | | [[SET]] | 1. 设置函数,写入属性时调用,默认值为 undefined |
修改 book.year 的值,触发 set,修改 edition 的值
let book = {
year_: 2017,
edition: 1
}
Object.defineProperty(book, 'year', {
get () {
return this.year_
},
set (newVal) {
if (newVal > 2017) {
this.year_ = newVal
this.edition += newVal - 2017
}
}
})
book.year = 2018
console.log(book.edition, 'book.edition'); // 2
只定义了 set 函数,没有定义获取函数,获取就会被忽略,严格模式会报错,反过来也一样
Object.defineProperty(book, 'year', {
get () {
return this.year_
},
set (newVal) {
if (newVal > 2017) {
this.year_ = newVal
this.edition += newVal - 2017
}
}
})
book.year = 2018
console.log(book.edition, 'book.edition'); // 2
console.log(book.year, 'book.year');
8.1.2 同时定义多个属性 Object.defineProperties
let person = {}
Object.defineProperties(person, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get () {
return this.year_
},
set (newVal) {
if (newVal > 2017) {
this.year_ = newVal
this.edition += newVal - 2017
}
}
}
})
console.log(person, 'person');
8.1.3 读取属性的特性 Object.getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor 能够获取对象的某个属性的属性描述符
// 读取属性的特性
let descriptor = Object.getOwnPropertyDescriptor(person, 'year_')
console.log(descriptor.value, 'descriptor.value'); // 2017
console.log(descriptor.configurable, 'descriptor.configurable'); // false
打印 set 获取的值是 undefined:
console.log(descriptor.set, 'descriptor.set'); // undefined
一次性读取对象的所有属性的描述,该方法会在每个自有属性上调用 Object.defineProperties 并在一个新对象中返回他们
// 一次性返回所有的属性
let descriptor2 = Object.getOwnPropertyDescriptors(person)
console.log(descriptor2, 'descriptor2');
8.1.4 合并对象 Object.assign
Object.assign: 把一个对象的值合并到另一个对象中。
简单的合并:
let src = {
id: 'src'
}
let dest = {
}
let result = Object.assign(dest, src)
console.log(result, 'result');
console.log(dest === result, 'dest === result'); // true
console.log(dest === src, 'dest === src'); // false
多个源对象:
let dest2 = {}
let result2 = Object.assign(dest2, {a: 1}, {b: 2})
console.log(result2, 'result2');
合并获取和设置函数,getter 函数和 setter 函数不能够被赋值
// 合并获取和设置函数:
let dest3 = {
set a(value) {
console.log(`Involked dest setter with param${value}`)
}
}
let src3 = {
get a () {
console.log('Involved src getter');
return 'foo'
}
}
let result3 = Object.assign(dest3, src3)
console.log(result3, 'result3');
后一个值会覆盖前一个重复的属性
// 覆盖
const dest4 = {
id: 'dest'
}
const result4 = Object.assign(dest4, {
id: 'src1',
a: 'foo'
}, {
id: 'src3',
a: 'foo2'
})
console.log(result4, 'result4');
浅复制,只会复制对象的引用
// 浅复制
let dest6 = {}
let src6 = {
a: {}
}
let result5 = Object.assign(dest6, src6)
console.log(result5.a === dest6.a, '浅复制'); // true
赋值若出错,则会同时抛出错误
// 尽力复制,不会回滚
let dest7 = {}
let src7 = {
a: 'foo',
get b () {
throw new Error('抛出错误')
},
c: 'bar'
}
try {
Object.assign(dest7, src7)
} catch (error) {
console.log(error, 'error');
}
console.log(dest7, 'dest7');
8.1.5 对象标识及相等判断 Object.is
注意,{} === {} 编辑器提示永远都是 false。注意+0 和-0 在不同的 JS 引擎表现不一样
// 正常情况
console.log(true === 1); // false
// console.log({} === {}, '{} === {}'); // false
console.log("2" === 2, '"2" === 2');
// 不同JS引擎表现不一样
console.log(+0 === -0, '+0 === -0');
console.log(+0 === 0, '+0 === 0');
console.log(-0 === 0, '-0 === 0');
// NaN的情况
console.log(NaN === NaN, 'NaN === NaN'); // false
console.log(isNaN(NaN), 'isNaN(NaN)'); // true
Object.is 方法能够正确处理这些情况
console.log(Object.is(true, 1), 'Object.is(true, 1)'); // false
console.log(Object.is({}, {}), 'Object.is({}, {})'); // false
console.log(Object.is("2", 2), 'Object.is("2", 2)'); // false
console.log(Object.is(+0, -0), 'Object.is(+0, -0)'); // false
console.log(Object.is(+0, 0), 'Object.is(+0, 0)'); // true
console.log(Object.is(-0, 0), 'Object.is(-0, 0)'); // false
console.log(Object.is(NaN, NaN), 'Object.is(NaN, NaN)'); // true
递归处理多个值,是正确的情况
function recursivelyCheckEqual (x, ...rest) {
return Object.is(x, rest[0]) && (rest.length < 2 || recursivelyCheckEqual(...rest))
}
console.log(recursivelyCheckEqual([], [], 2, 2), 'recursivelyCheckEqual([], [], 2, 2)'); // false
console.log(recursivelyCheckEqual(2, 2), 'recursivelyCheckEqual(2, 2)'); // true
8.1.6 增强的对象语法
属性值简写
let name = 123;
let person = {
name // JS引擎会去作用域查找name的变量
}
可计算属性
[] 里面写表达式
const nameKey = 'name'
const ageKey = 'age'
const jobKey = 'job'
let person2 = {}
// 变量作为属性,以往只能通过这样的方式去赋值
person2[nameKey] = 'Matt'
person2[ageKey] = '27'
person2[jobKey] = 'Software engineer'
console.log(person2, 'person2');
// 可计算属性:
let person3 = {
[nameKey]: 'Matt',
[ageKey]: '27',
[ageKey]: 'Software engineer',
}
console.log(person3, 'person3');
// 可计算属性还可以用函数
let uniqueToken = 0;
function getUniqueKey (key) {
return `${key}_${uniqueToken++}`
}
let person4 = {
[getUniqueKey(nameKey)]: 'Matt',
[getUniqueKey(ageKey)]: '27',
[getUniqueKey(jobKey)]: 'Software engineer',
}
console.log(person4, 'person4');
注意:可计算表达式中创建的任何错误都会中断对象创建
简写方法名
直接:函数名 () {}
// 旧的写法
let person5 = {
sayName: function (name) {
console.log(`My name is ${name}`);
}
}
person5.sayName('Matt')
// 简写方法名
let person6 = {
sayName (name) {
console.log(`My name is ${name}`);
}
}
person6.sayName('Matt')
8.1.7 对象解构
不使用对象解构
let person = {
name: 'Matt',
age: 27
}
let personName = person.name,
personAge = person.age
console.log(personName, 'personName');
console.log(personAge, 'personAge');
使用对象解构
// 使用对象解构
const {name, age} = person
console.log(name, 'name');
console.log(age, 'age');
不存在的属性,默认值为undefined
const { job } = person
console.log(job, 'job');
给默认值
const {job1 = 'Engineer'} = person
console.log(job1, 'job1');
对象解构的上下文,原始值会被当成对象
// 对象解构的上下文,原始值会被当成对象
let { length } = 'foobar'
console.log(length, 'length'); // 6
let { constructor: c } = 4
console.log(c === Number, 'c === Number'); // true
null和undefined不能被解构
let { _ } = null;
let { _1 } = undefined;
给事先声明的变量必须在解构表达式中声明,注意,下面加括号,是整个表达式外面加,({name: personName2} = person2)
let personName2, personAge2
// 没加分号这里报错了
let person2 = {
name: 'Matt2',
age: '28'
};
({name: personName2, age: personAge2} = person2)
console.log(personName2, 'personName2');
console.log(personAge2, 'personAge2');
嵌套解构
如下person3.job 也是一个对象,不影响嵌套。注意解构是浅拷贝,所以修改 person.job.title 值,personCopy.job 也会发生变化
let person3 = {
name: 'Matt',
age: '27',
job: {
title: 'Software Engineer'
}
}
let personCopy = {};
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person3)
console.log(personCopy, 'personCopy');
person3.job.title = 'Hacker'
console.log(personCopy, 'personCopy');
解构时可以使用嵌套解构,如下把 job 里面的 title 属性解构出来
let person4 = {
name: 'Matt',
age: '27',
job: {
title: 'Software Engineer'
}
}
let {job: { title } } = person4
console.log(title, 'title');
外层属性没有定义,不能使用嵌套解构,不论是源对象还是目标对象
// 外层属性没有定义,不能使用嵌套解构
let {
foo: { title2 }
} = person4
console.log(title2, 'title2');
foo.bar 不存在,person4Copy.bar 也不存在
let person4Copy = {};
({
foo: {
bar: person4Copy.bar
}
} = person4)
({
job: {
title: person4Copy.job.title
}
} = person4)
部分解构
若涉及多个解构赋值,第一个对,第二个报错,则后面的都将中断。如下,foo 是 undefined,所以后续的 age 也不能解构成功
let person5 = {
name: 'Matt',
age: '27'
}
let personName3, personAge3, personBar;
try {
({ name: personName3, foo: {bar: personBar}, age: personAge3 } = person)
} catch (error) {
console.log(error, 'error');
console.log(personName3, personBar, personAge3, 'personName3, personBar, personAge3');
}
函数解构赋值
function printPerson (foo, {name, age}, bar) {
console.log(arguments, 'arguments printPerson');
console.log(name, age, 'name age');
}
function printPerson2 (foo, {name: personName, age: personAge}, bar) {
console.log(arguments, 'arguments printPerson2');
console.log(personName, personAge, 'personName, personAge');
}
printPerson('1st', person5, '2nd')
printPerson2('1st', person5, '2nd')
8.2 创建对象
8.2.2 工厂模式
工厂模式,每次调用都会返回一个对象,这个对象包含三个参数和一个方法。但是存在问题:没有解决“新创建的对象是什么类型”
// 工厂模式
function createPerson (name, age, job) {
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name, 'this.name');
}
return o
}
let person1 = createPerson('Nicholas', 29, 'Software Engineer')
let person2 = createPerson('Greg', 27, 'Doctor')
8.2.3 构造函数模式
如下是构造函数来创建对象的方式,Person 方式代替了 createPerson 工厂函数
// 构造函数模式
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name, 'this.name');
}
}
let person3 = new Person('Nicholas2', 29, 'Software Engineer')
let person4 = new Person('Greg2', 27, 'Doctor')
person3.sayName()
person4.sayName()
实际上,Person 内部的代码和createPerson 几乎是一样的,区别是:
- 没有直接显式创建一个对象,没有 return
- 属性和方法直接赋值给了 this
- 函数名称 Person 首字母应该大写,非构造函数名称首字母则小写
- 需要使用 new 操作符
new 操作符做了哪些事情呢?
- 在内存中创建一个新对象
- 新对象的 proto 指向为构造函数的 Prototype
- 构造函数内部 this 赋值为新对象
- 执行构造函数内部代码,给新对象添加属性
- 如果构造函数最后 return 的是非空对象,则返回他,否则返回内存中新建的对象
constructor 属性:在实例身上,指向构造函数,标识对象的类型
console.log(person3.constructor === Person, 'person3.constructor === Person'); // true
console.log(person4.constructor === Person, 'person4.constructor === Person'); // true
instanceof:确定对象类型。所以,记住,所有自定义对象都继承自 Object 构造函数
console.log(person3 instanceof Person, 'person3 instanceof Person'); // true
console.log(person3 instanceof Object, 'person3 instanceof Object'); // true
console.log(person4 instanceof Person, 'person3 instanceof Person'); // true
console.log(person4 instanceof Object, 'person3 instanceof Object'); // true
函数表达式也可以用于声明构造函数,注意箭头函数不行
let Person2 = function (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name, 'this.name');
}
}
let person5 = new Person2('Mike', 27, 'Engineer')
console.log(person5, 'person5');
console.log(person5 instanceof Person2, 'person5 instanceof Person2');
new 操作符后面的()可加可不加
let person6 = new Person2
console.log(person6, 'person6');
1. 构造函数和普通函数的区别:
唯一的区别就是调用方式的不一样,构造函数使用 new 关键字,不用 new 的方式调用,他就是普通函数。如下,Person3()直接调用,就当成普通函数,此时函数里面的 this 会指向 window,就把 sayName 挂载到全局 window 对象了。(浏览器是 window)
let Person3 = function (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name, 'this.name');
}
}
Person3('xhg', 24, 'Engineer job')
window.sayName()
通过.call()的方式调用构造函数
let o = new Object()
Person.call(o, 'Criston', 25, 'Nurse')
o.sayName()
2. 构造函数的问题
问题是:你看下面的 person7 和 person8 都有一个 自己的 sayName 方法,每次创建一个实例,实例内部都会创建一个新的方法,这样就很重复
let Person4 = function (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name, 'this.name');
}
}
let person7 = new Person4('xhg7', '24', 'Engineer')
let person8 = new Person4('xhg7', '24', 'Engineer')
他们的方法是不相等的
let person7 = new Person4('xhg7', '24', 'Engineer')
let person8 = new Person4('xhg7', '24', 'Engineer')
console.log(person7.sayName === person8.sayName, 'person7.sayName === person8.sayName');
解决办法1:用外部公用的方法。如下,外部声明一个函数,构造函数内部直接赋值为这个外部的构造函数
let Person5 = function (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = sayName
}
function sayName () {
console.log(this.name, 'this.name');
}
let person9 = new Person5('xhg9', '24', 'Engineer')
let person10 = new Person5('xhg9', '24', 'Engineer')
console.log(person9.sayName === person10.sayName, 'person7.sayName === person8.sayName'); // true
但这样存在的问题:会污染全局环境,而且如果构造函数内部有很多方法,外部就要声明很多,很麻烦。而且外部声明很多方法,会导致一个类的方法分散开来,就很不方便
8.2.4 原型模式
每个函数都会有一个 prototype 属性,这个属性指向一个对象,我们能在该对象上共享属性和方法。如下 person1 和 person2 实例都有 name、age,并且他们的方法是相等的
// 原型模式
function Person () {
}
Person.prototype.name = 'Matt'
Person.prototype.age = '27'
Person.prototype.sayName = function () {
console.log(this.name, 'this.name');
}
let person1 = new Person()
let person2 = new Person()
console.log(person1.name, 'person1.name'); // 'Matt'
console.log(person2.name, 'person2.name'); // 'Matt'
console.log(person1.sayName === person2.sayName, 'person1.sayName === person2.sayName');
1. 理解原型
- 每个构造函数创建时,会立即创建一个 prototype 属性,指向原型对象
- 每个原型对象,都有一个属性constructor 指向构造函数
- 每个对象实例身上,都有 proto 属性,指向原型对象。实际是实例内部的指针属性 [[Prototype]] 被赋值为构造函数的原型对象,因为脚本无法访问这个指针属性,所以浏览器才暴露了 proto 属性
- 注意,构造函数、原型对象、实例对象是三个完全不同的对象
- 构造函数能够初始化实例对象,但是实例对象反过来与构造函数没有直接联系
console.log(typeof Person.prototype, 'typeof Person.prototype'); // object
console.log(Person.prototype, 'Person.prototype');
constructor属性指回构造函数
console.log(Person.prototype.constructor === Person, 'Person.prototype.constructor === Person'); // true
正常的原型链终止与 Object 的原型对象
// 正常的原型链终止与 Object 的原型对象
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
打印 Object.prototype
console.log(Person.prototype.__proto__);
同个构造函数创建的两个实例对象,共享一个原型对象
console.log(person1.__proto__ === person2.__proto__); // true
instanceof 判断
// instanceof
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
isPrototypeOf 判断实例身上是否有 [[Prototype]] 也就是 proto,指向原型对象。如下返回 true,代表 person1 的 proto 属性就是指向 Person.prototype
// isPrototypeOf 判断实例身上是否有 [[Prototype]] 也就是 __proto__,指向原型对象
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
Object.getPrototypeOf()返回参数的原型对象,能够拿到原型对象上的属性和方法。这些方法和属性和构造函数没有关系,但是在实例对象上能够访问到
// Object.getPrototypeOf()返回参数的内部特性
console.log(Object.getPrototypeOf(person1) === Person.prototype, 'Object.getPrototypeOf(person1)');
setPrototypeOf 方法:修改实例的私有特性 [[Prototype]],也就是 proto。注意调用该方法会严重影响代码性能,会涉及所有访问了哪些修改过 [[Prototype]] 对象的代码
let biped = {
numLegs: 2
}
let person = {
name: 'Matt'
}
Object.setPrototypeOf(person, biped)
console.log(Object.getPrototypeOf(person) === biped); // true
Object.create: 为了避免使用setPrototypeOf 方法,可以用 create 方法创建新对象
let biped2 = {
numLegs: 2
}
let person3 = Object.create(biped2)
person3.name = 'Matt'
console.log(person.name, 'person.name'); // 'Matt'
console.log(person.numLegs, 'person.numLegs'); // 2
console.log(Object.getPrototypeOf(person3) === biped2); // true
2. 原型层级
通过对象访问属性,会按照属性的名称开始搜索,先在实例对象本身开始查找。如果找到了则返回,如果没找到则搜索会沿着指针去到原型对象继续找,找到则返回。如果还找不到,原型身上也有一个 proto 属性,继续向上找
注意,实例对象上的属性会覆盖原型对象上的同名属性。能够覆盖,但是不能修改
function Person2 () {}
Person2.prototype.name = 'Nicholas'
Person2.prototype.age = '27'
let person5 = new Person2()
let person6 = new Person2()
person5.name = 'Greg'
console.log(person5.name, 'person5.name'); // 'Greg'
delete 实例对象上的属性后,还是会继续去访问原型对象
delete person5.name
console.log(person5.name, 'person.name delete后'); // 'Nicholas'
person1.hasOwnProperty 判断属性是否是实例自己身上的
function Person2 () {}
Person2.prototype.name = 'Nicholas'
Person2.prototype.age = '27'
let person5 = new Person2()
let person6 = new Person2()
person5.name = 'Greg'
console.log(person5.name, 'person5.name'); // 'Greg'
+console.log(person5.hasOwnProperty("name"), 'person5.hasOwnProperty(name)'); // true
delete person5.name
console.log(person5.name, 'person.name delete后'); // 'Nicholas '
+console.log(person5.hasOwnProperty("name"), 'person5.hasOwnProperty(name)'); // false
Object.getOwnPropertyDescriptors() 该方法只对示例对象的自有属性有效果,返回对象的属性描述符
// 注意,实例对象上的属性会覆盖原型对象上的同名属性
function Person2 () {}
Person2.prototype.name = 'Nicholas'
Person2.prototype.age = '27'
let person5 = new Person2()
let person6 = new Person2()
person5.name = 'Greg'
console.log(person5.name, 'person5.name'); // 'Greg'
console.log(person5.hasOwnProperty("name"), 'person5.hasOwnProperty(name)'); // true
+console.log(Object.getOwnPropertyDescriptors(person5), 'Object.getOwnPropertyDescriptor(person5)');
delete person5.name
console.log(person5.name, 'person.name delete后'); // 'Nicholas '
console.log(person5.hasOwnProperty("name"), 'person5.hasOwnProperty(name)'); // false\
+console.log(Object.getOwnPropertyDescriptors(person5), 'Object.getOwnPropertyDescriptor(person5)');
3. 原型和 in 操作符
in 操作符可以单独使用
function Person1 () {}
Person1.prototype.name = 'Nicholas'
Person1.prototype.age = '27'
let person1 = new Person1()
let person2 = new Person1()
// in 操作符可以单独使用
console.log("name" in person1, '"name" in person1'); // true
console.log(person1.hasOwnProperty("name"), '"name" in person1'); // false
person1.name = "Nicholas"
console.log("name" in person1, '"name" in person1'); // true
console.log(person1.hasOwnProperty("name"), '"name" in person1'); // true
如下方法封装 判断属性是否只在原型上。删除实例自己的属性后,该方法返回 true,属性只在原型上;给实例赋值后,该方法返回 false,实例对象上的属性遮蔽了原型对象上的属性
function hasPrototypeProperty (object, name) {
return !object.hasOwnProperty(name) && (name in object)
}
delete person1.name
console.log(hasPrototypeProperty(person1, "name"), ''); // true
person1.name = 'Greg'
console.log(hasPrototypeProperty(person1, "name"), ''); // false
在 for-in 循环中使用 in 操作符,可以通过对象访问,可以被枚举的属性都会返回
let person3 = new Person1()
console.log(person3, 'person3');
person3.name = 'xhg'
for (const key in person3) {
console.log(key, person3[key], 'key person3[key]');
}
Object.keys()获得的是对象上所有可枚举的属性 ,不会去找原型对象上。如下只返回了 name
let person4 = new Person1()
person4.name = "Rob"
let keys2 = Object.keys(person4)
console.log(keys2, 'keys2'); // ['name']值
如下对 Person1.prototype,返回的是其上面的 name 和 age 属性
let keys = Object.keys(Person1.prototype)
console.log(keys, 'keys'); // ['name', 'age'] 'keys'
Object.getOwnPropertyNames 如果想列出所有实例属性,无论是否可以枚举
// 如果想列出所有实例属性,无论是否可以枚举
let keys3 = Object.getOwnPropertyNames(Person1.prototype)
console.log(keys3, 'keys3 Person1.prototype'); // ['constructor', 'name', 'age'] '
符号键:getOwnPropertySymbols()
let person5 = {
[Symbol('k1')]: 'k1',
[Symbol('k2')]: 'k2',
}
console.log(person5, 'person5');
console.log(Object.getOwnPropertySymbols(person5)); // [Symbol(k1), Symbol(k2)]
枚举顺序:Object.keys()和 for-in 枚举的顺序不确定,Object.getOwnProperty 和 Object.getOwnPropertySymbols 和 Object.assign 的枚举顺序是确定的,先升序枚举数值键,然后以插入顺序枚举字符串和符号键。注意, '11' '22'这样的也是数值键
let k1 = Symbol('k1')
let k2 = Symbol('k2')
let o = {
1: 1,
first: 'first',
[k2]: 'sym2',
third: 'second',
0: 0,
'22': '22'
}
o[k1] = 'sym2'
o[3] = 3
o.second = 'third'
o[2] = 2
o['11'] = '11'
console.log(Object.getOwnPropertyNames(o)); // ['0', '1', '2', '3', '11', '22', 'first', 'third', 'second']
console.log(Object.getOwnPropertySymbols(o)); // [Symbol(k2), Symbol(k1)]
8.2.5 对象迭代
ES2017 提供两个方法:Object.values() 返回对象值的数组和 Object.entries()返回对象键值对的数组
const o = {
foo: 'bar',
baz: 1,
qux: {}
}
console.log(Object.values(o), 'Object.values(o)');
console.log(Object.entries(o), 'Object.entries(o)');
是浅拷
// 忽略符号属性
o[Symbol('s1')] = 'ssss1'
console.log(Object.values(o), 'Object.values(o)');
忽略符号属性
1. 其他原型语法
更加高效的修改原型的方式,直接将原型赋值为一个对象字面量。问题只有一个,constructor 直接指向了 Object,而不是指向 Person 构造函数,如下 constructor 的赋值
function Person () {}
Person.prototype = {
name: 'Matt',
age: '27',
sayName () {
console.log(this.name, 'this.name');
}
}
let person1 = new Person()
console.log(person1 instanceof Person, 'person1 instanceof Person'); // true
console.log(Person.prototype.constructor === Person); // false
console.log(Person.prototype.constructor === Object); // true
记得这样去赋值,原生的 constructor 就是不可读的,所以设置 enumerable 为 false
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
console.log(Person.prototype, 'Person.prototype');
2. 原型的动态性
friend 实例创建后,才给原型对象增加sayHi 函数,能够访问到。是因为实例对象的指针就是指向原型对象的,自身找不到就会去原型对象上找
function Person () {}
let friend = new Person()
Person.prototype.sayHi = function () {
console.log('hi');
}
但如果重新给原型对象赋值一个新对象,对象实例会仍旧指向旧的原型对象
// 但如果重新给原型对象赋值一个新对象,对象实例会仍旧指向旧的原型对象
function Person2 () {}
let friend2 = new Person()
Person.prototype = {
constructor: Person,
name: 'xhg',
age: '27',
job: 'Software Engineer',
sayName() {
console.log(this.name, 'this.name');
}
}
friend2.sayName()
修改之前
修改之后
3. 原生对象原型
给原生对象,比如 String.prototype 增加startsWith 方法,增加后,后续所有的字符串都可以使用这个方法
String.prototype.startsWith = function (value) {
return this.indexOf(text) === 0
}
let msg = "Hello World"
console.log(msg.startsWith("Hello"));
注意:这样做可能会造成命名冲突,推荐创建一个自定义的类。
4. 原型的问题
原型对象上的引用类型,如下创建 friend1 和 friend2 的实例,往 friend1 的 friends 数组里面增加了 van 值,friend2 里面对应也会增加这个值,按理来说我们希望这两个实例里面的好友是不同,除非我们希望每个实例他的这个 friends 值都是一样
function Person2 () {}
Person.prototype = {
constructor: Person,
name: 'xhg',
age: '27',
job: 'Software Engineer',
friends: ["Shelby", "Court"],
sayName() {
console.log(this.name, 'this.name');
}
}
let friend1 = new Person()
let friend2 = new Person()
friend1.friends.push("van")
console.log(friend2.friends, 'friend2.friends');
注意:实际开发中,通常不单独使用原型链
8.3 继承
8.3.1 原型链
原型链:
- 每个构造函数有一个 prototype 属性指向原型对象
- 原型对象身上有个 constructor 属性指回构造函数
- 每个构造函数通过 new 创建一个实例
- 实例有一个 proto 属性指向原型对象
- 如果原型对象本身也是另一个类型的实例,那么他也有一个 proto 属性指向另一个原型对象,相应另一个原型对象也有 constructor 属性指向该构造函数
Student.prototype = new Person()这行代码,让 Student 的原型对象指向了 Person 构造函数的原型对象。注意,sing 方法在 Person 的原型对象上,而 emotion 属性在 Student 的原型对象上
function Person() {
this.emotion = ['吃饭', '睡觉']
this.sleep = function () {
console.log('sleep');
}
}
Person.prototype.sing = function () {
console.log('I can sing');
}
function Student(id) {
this.id = id
}
+Student.prototype = new Person()
let stu = new Student('111')
stu.sing() // 'I can sing'
console.log(stu.emotion, 'stu.emotion'); // ['吃饭', '睡觉']
如下图,stu 要访问 emotion 时,借助 proto 访问路径:stu、Student.Prototype,在Student.Prototype 身上找到了。stu 要访问 sing 方法,借助 proto 访问路径:stu、Student.Prototype、Person.Prototype
1. 默认原型
每个构造函数的 proto 最终都指向 Object.prototype,如下图右方
2. 原型与继承关系
instanceof:实例原型链中出现过相应的构造函数
console.log(stu instanceof Student);
console.log(stu instanceof Person);
console.log(stu instanceof Object);
isPrototypeOf:原型链中每个原型都可以调用,只要原型链中包含这个实例
console.log(Object.prototype.isPrototypeOf(stu), 'Object.isPrototypeOf(stu)');
console.log(Person.prototype.isPrototypeOf(stu), 'Person.isPrototypeOf(stu)');
console.log(Student.prototype.isPrototypeOf(stu), 'Student.isPrototypeOf(stu)');
3. 关于方法
增加新方法、覆盖方法。在上面的代码基础上,增加如下,sing 方法会覆盖 Person.prototype.sing,stu 实例会用他,而 dance 方法是新增加的方法。
console.log('增加新方法、覆盖方法 -------------');
Student.prototype.dance = function () {
console.log('Student I can dance');
}
Student.prototype.sing = function () {
console.log('Student I can sing');
}
stu.sing()
stu.dance()
注意,对象字面量覆盖原型,会把直接的赋值都给覆盖掉。如下,在赋值为new Person2()后,又赋值为{},则原本的 sing 方法将访问不到了
function Person2() {
this.emotion = ['吃饭', '睡觉']
this.sleep = function () {
console.log('sleep');
}
}
Person2.prototype.sing = function () {
console.log('I can sing');
}
function Student2(id) {
this.id = id
}
Student2.prototype = new Person2()
Student2.prototype = {
write () {
console.log('不好的写法 I can sing');
}
}
let stu2 = new Student2()
stu2.sing()
4. 原型链的问题
- 上面 8.2.5 提到过的引用数据类型的问题
- 子构造函数无法向父构造函数传递参数
8.3.2 盗用构造函数
在子类构造函数中通过.call 调用父类构造函数,传入 this,相当于在子构造函数里面执行了父构造函数的代码
function Person() {
this.emotion = ['吃饭', '睡觉']
this.sleep = function () {
console.log('sleep');
}
}
Person.prototype.sing = function () {
console.log('I can sing');
}
function Student(id) {
+ Person.call(this)
this.id = id
}
let stu = new Student('111')
console.log(stu.emotion, 'stu.emotion');
console.log(stu.id, 'stu.id');
1. 解决引用类型共享 问题
stu1.emotion.push('打豆豆')
console.log(stu1.emotion, 'stu1.emotion')
console.log(stu3.emotion, 'stu3.emotion')
2. 传递参数
子调父时,传递参数,在父类中接受;这些属性,就是在子类身上,如下图
function Person2(name) {
+ this.name = name
this.emotion = ['吃饭', '睡觉']
this.sleep = function () {
console.log('sleep');
}
}
Person2.prototype.sing = function () {
console.log('I can sing');
}
function Student2(id) {
+ Person2.call(this, 'Mike')
this.id = id
}
let stu2 = new Student2('111')
console.log(stu2.name, 'stu.name');
3. 盗用构造函数的问题
必须在构造函数内部定义属性和方法,如果在父类的原型上定义属性和方法,子类将访问不到,所以该继承不会单独使用
stu2.sing()
8.3.3 组合继承
组合继承:
- 不仅可以往父构造函数传参数、每个实例都有自己的引用数据
- 还可以使用父构造函数原型对象上的方法
- 子构造函数还可以自己定义方法
- 他是最常用的继承方法
function Person(name) {
this.name = name
this.emotion = ['吃饭', '睡觉']
this.sleep = function () {
console.log('sleep');
}
}
Person.prototype.sing = function () {
console.log('I can sing');
}
function Student(id) {
+ Person.call(this, 'Mike')
this.id = id
}
+Student.prototype = new Person()
Student.prototype.dance = function () {
console.log('dance');
}
let stu1 = new Student('111')
let stu2 = new Student('222')
stu1.emotion.push('打豆豆')
console.log(stu1.emotion, 'stu1.emotion');
console.log(stu2.emotion, 'stu2.emotion');
stu1.sing()
stu2.dance()
注意子类的 Constructor 会指向 Person 构造函数,需要我们进行修改
Student.prototype = new Person()
+Student.prototype.constructor = Student
8.3.4 原型式继承
原型式继承:不需要单独创建构造函数,但是仍旧想要在对象间共享信息
如下代码,object 函数接受一个对象,在函数中,创建另一个函数 F,其 prototype 指向传参 o,并返回函数 F 的实例。那么 person1 和 person2 的 proto 指向的是 person。这样这两个实例会共享这个原型对象上面的属性和方法
function object (o) {
function F () {}
F.prototype = o
return new F()
}
let person = {
name: 'Nicholas',
friends: [
"Shelby",
"Court",
"Van"
]
}
let person1 = object(person)
person1.name = 'Jek'
person1.friends.push('Mike')
let person2 = object(person)
person2.name = 'Sarah'
person2.friends.push('John')
console.log(person.name, 'person.name'); // 'Nicholas'
console.log(person.friends, 'person.friends'); // ['Shelby', 'Court', 'Van', 'Mike', 'John']
如上代码,效果等同于使用 Object.create()方法,该方法,将上述行为标准化
let person3 = Object.create(person)
let person4 = Object.create(person)
person3.friends.push('Bum')
person4.friends.push('Amy')
console.log(person, 'person');
8.3.5 寄生式继承
寄生式继承:创建一个函数接受 original 对象的参数,以某种方式增强对象,然后返回这个对象。这个方式:接受一个 original 对象, original 对象传到另一个函数,返回一个新对象 target,给这新对象 target 增加属性和方法。适合不在乎类型和构造函数的场景
和原型式继承有点像,如下createAnother 函数会接受original 参数,借助 object 函数返回一个新的 clone 对象,然后给 clone 对象增加新的sayHi 方法,再返回clone 对象。这样anotherPerson 对象身上有 sayHi 方法,还能使用到 person 身上的所有属性和方法,因为其 proto 属性指向 person 对象
function object (o) {
function F () {}
F.prototype = o
return new F()
}
let person = {
name: 'Nicholas',
friends: [
"Shelby",
"Court",
"Van"
]
}
+ function createAnother (original) {
let clone = object(original)
+ clone.sayHi = function () {
console.log('sayHi');
}
return clone
}
let anotherPerson = createAnother(person)
anotherPerson.sayHi() // "sayHi"
console.log(anotherPerson, 'anotherPerson');
+ console.log(anotherPerson.__proto__ === person, 'anotherPerson.__proto__ === person'); // true
缺点:会导致添加的函数难以重用,占用内存空间,和构造函数模式类似
8.3.6 寄生式组合继承
分析组合继承问题:如下Person 构造函数,在 new Person()赋值的时候执行一次,在 Student 构造函数内部通过.call 也执行了一次,执行了两次,造成结果如下图,stu 实例对象身上也有这几个属性(.call()执行获得),原型对象上也有这几个属性(= new Person()获得)
function Person(name) {
this.name = name
this.emotion = ['吃饭', '睡觉']
this.sleep = function () {
console.log('sleep');
}
}
Person.prototype.sing = function () {
console.log('I can sing');
}
function Student(id) {
Person.call(this, 'Mike')
this.id = id
}
Student.prototype = new Person()
Student.prototype.constructor = Student
如下,声明一个空的构造函数 Super,其 prototype 属性指向 Person2 的 prototype 属性,接着在 Student2.prototype 赋值为 new Super()实例,这样 Person2 构造函数只执行了一次
function Person2(name) {
this.name = name
this.emotion = ['吃饭', '睡觉']
this.sleep = function () {
console.log('sleep');
}
// 这里只执行了一次
console.log(name, 'Person2 name');
}
Person2.prototype.sing = function () {
console.log('I can sing');
}
function Student2(id) {
Person2.call(this, 'Mike')
this.id = id
}
+function Super () {}
+Super.prototype = Person2.prototype
+Student2.prototype = new Super()
Student2.prototype.constructor = Student
let stu2 = new Student2('11')
console.log(stu2, 'stu2');
注意观察,emotion name sleep 在 stu 实例上,但是其原型 prototype 上没有,然后 proto.proto 能访问到 sing 方法
8.4 类
8.4.1 类定义
类定义两种方式:
类声明,注意首字母要大写,表明这是一个类
class Person {}
类表达式
const Animal = class {}
函数表达式不能提升,但是var关键字会提升变量,所以这里是undefined,如果是let会直接报错
console.log(FunctionExpression, 'FunctionExpression'); // undefined
var FunctionExpression = function () {}
console.log(FunctionExpression, 'FunctionExpression'); // ƒ () {} 'FunctionExpression'
函数声明有提升
console.log(FunctionDeclaration, 'FunctionDeclaration');
function FunctionDeclaration () {}
console.log(FunctionDeclaration, 'FunctionDeclaration');
类表达式不会提升声明;类声明会报错
// 类表达式不会提升声明
console.log(ClassExpression, 'ClassExpression'); // undefined
var ClassExpression = class {}
console.log(ClassExpression, 'ClassExpression'); // class {} 'ClassExpression'
// 类声明
// console.log(ClassDecaration, 'ClassDeclaration'); // 报错
class ClassDeclaration {}
console.log(ClassDeclaration, 'ClassDeclaration'); // undefined
类受块级作用域影响
{
function function2 () {}
class Person2 {}
}
console.log(function2, 'function2');
console.log(Person2, 'Person2');
类表达式名称可选, 如下 class 后面的名称可选,外部不能直接访问这个名称,只能 Person2.name,在内部才可以 Person_.name
let Person2 = class Person_ {
identify() {
console.log(Person2.name, Person_.name); // 能访问到
}
}
let p = new Person2()
p.identify()
console.log(Person_, 'Person_'); // is not defined
8.4.2 类构造函数
1. 实例化
- 类构造函数可以不写。
- 如果调用时传参,则在 constructor 中接受。
- 使用 new 关键字调用类,JS 解释器知道此时应该调用 constructor 函数进行实例化。(new 的操作过程还是和之前一样)
class Animal {
}
class Person {
constructor (name) {
this.name = name
}
}
let cat = new Animal()
let person = new Person('xhg')
console.log(cat, 'cat');
console.log(person, 'person');
若不传参,类名后面括号可以不写
let dog = new Animal
console.log(dog, 'dog');
默认情况,constructor 构造函数执行完,会返回 this 对象,如果返回其他对象,则该对象和类的关联将无法通过 instanceof 检测出来
class Person2 {
constructor (override) {
this.foo = 'foo'
if (override) {
return {
bar: 'bar'
}
}
}
}
let p1 = new Person2(),
p2 = new Person2(true);
console.log(p1, 'p1');
console.log(p2, 'p2');
console.log(p2 instanceof Person2, 'p2 instanceof Person2'); // false
类构造函数(constructor 函数)和 普通构造函数区别:
- 类构造必须使用 new 来调用,否则报错;普通构造函数可以不用 new,不用 new 就会在以全局的 this 作为内部对象
let p3 = Person2()
console.log(p3, 'p3');
如下是构造函数的打印
function Person3 () {
this.myAge = 14
}
let p4 = Person3()
console.log(p4, 'p4');
console.log(window.myAge, 'window.myAge');
使用 new 对类构造函数的引用创建一个新实例。如下可以这样写
class Person4 {}
let p5 = new Person4()
let p6 = new p5.constructor()
console.log(p6, 'p6');
2. 把类当成特殊函数
类就是一种特殊的函数,通过 typeof 关键字可以判断出
class Person {}
console.log(typeof Person, 'typeof Person'); // function typeof Person
类也有 prototype 属性,而这个类.prototype 也有一个 constructor 属性指回类本身
console.log(Person.prototype, 'Person.prototype');
console.log(Person.prototype.constructor === Person); // true
可以使用 instanceof 关键字来检测构造函数原型是否存在实例的原型链中
class Person2 {}
let p = new Person2()
console.log(p instanceof Person2); // true
直接对 Person.contructor() 使用 instanceof 关键字时,会返回 false。也就是说p1 instanceof Person3.constructor 是 false。
class Person3 {}
let p1 = new Person3()
// constructor就是从原型对象身上去拿的
console.log(p1.constructor, 'p1.constructor');
console.log(p1.constructor === Person3, 'p1.constructor'); // true
console.log(p1 instanceof Person3, 'p1 instanceof Person3'); // true
// 这个打印出来是构造函数,和Person.prototype.constructor不一样
console.log(Person3.constructor, 'Person3.constructor');
console.log(p1 instanceof Person3.constructor); // false
类是 JS 的一等公民,可以像其他对象或者函数引用一样把类当做参数传递
let classList = [
class Cat {
constructor (id) {
this._id = id
console.log(`instance ${this._id}`);
}
}
]
function createInstance (classDefinition, id) {
return new classDefinition(id)
}
let foo = createInstance(classList[0], 3141)
console.log(foo, 'foo');
打印
类可以立即实例化
let p3 = new class Foo {
constructor (x) {
console.log(x, 'x');
}
}('bar')
console.log(p3, 'p3');
8.4.3 实例 类 构造函数成员
1. 实例成员
- 通过 new 调用类标识符,会执行构造函数,在构造函数内部,可以为新创建的实例添加自由属性
- 每个实例都对应唯一的成员对象。如下代码中,p1 和 p2 的 name 值、sayName、nickName 都不相等
class Person {
constructor () {
this.name = new String('Jack')
this.sayName = () => {
console.log(this.name, 'this.name');
}
this.nickName = ['Jake', 'J-Dog']
}
}
let p1 = new Person()
let p2 = new Person()
p1.sayName()
p2.sayName()
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nickName === p2.nickName); // false
p1.name = p1.nickName[0]
p2.name = p2.nickName[1]
p1.sayName()
p2.sayName()
2. 原型方法与访问器
如下代码中,this.locate 会在实例上增加一个 locate 方法,类块中定义的 locate 方法,会在 prototype 上定义,当我们访问 p.locate()时,会先看对象实例上有无这个方法,如果没有则会去原型对象身上找
class Person2 {
constructor () {
// 添加到this的所有内容,都会定义在不同实例上
this.locate = () => {
console.log('instance');
}
}
// 添加到类块中的内容,都会定义在类的原型上
locate () {
console.log('prototype');
}
}
let p = new Person2()
p.locate() // instance
Person2.prototype.locate() // prototype
错误写法,直接在类里面定义一个数据
class Person3 {
name: '1234'
}
类方法等同于对象属性,因此可以使用字符串、符号、计算的值作为键。如下定义了字符串、Symbol、计算后值作为的 Key
const symbol = Symbol('mySymbol')
class Person4 {
stringKey () {
console.log('invoked stringKey');
}
[symbol] () {
console.log('invoked symbolKey');
}
['computed' + 'Key'] () {
console.log('invoked computedKey');
}
}
let p4 = new Person4()
console.log(p4, 'p4');
支持获取和设置访问器,和普通对象一样。如下对 Person5 类设置了 set name 和 get name 的访问器方法,当修改或者访问 name 属性是会触发对应方法
class Person5 {
set name (newVal) {
this.name_ = newVal
}
get name () {
console.log('get name 执行了');
return this.name_
}
}
let p5 = new Person5()
p5.name = 'Jake'
console.log(p5.name, 'p5.name');
3. 静态类方法
- 静态类方法,直接通过类标识符来调用,不需要实例
- 通过 staic 关键字
- 注意如下代码, 分别测试了实例的 locate 方法 和 Person6.prototype.locate()方法 和 Person6.locate()方法
class Person6 {
constructor () {
this.locate = () => {
console.log('instance', this);
}
}
locate () {
console.log('prototype', this);
}
static locate () {
console.log('class', this);
}
}
p.locate()
Person6.prototype.locate()
Person6.locate()
适合定义实例工厂
class Person7 {
constructor (age) {
this.age_ = age
}
sayAge () {
console.log(this.age_);
}
static create () {
// 传入随机年龄,返回实例
return new Person7(Math.floor(Math.random() * 100))
}
}
console.log(Person7.create(), 'Person7.create');
4. 非函数原型和类成员
类里面,除了在 constructor 里面通过 this 添加数据,无其他方法显示的添加数据。比如我们说过如下的写法是错误的
class Person {
name: '123'
}
可以在外部,给类添加数据成员,或者在原型上添加数据成员
class Person8 {
sayName () {
console.log(`${Person8.greeting} ${this.name}`);
}
}
Person8.greeting = 'My name is'
Person8.prototype.name = 'Jake'
let p8 = new Person8()
p8.sayName() // My name is Jake
注意:之所以不允许在类里面支持直接添加数据成员,是希望每个对象实例的数据成员都独立,所以必须经过 constructor 里面的 this 赋值
5. 迭代器与生成器方法
类定义语法,支持在原型和类本身定义生成器方法。如下代码中,在原型上定义createNicknameIterator 迭代器方法,在类上定义createJobIterator 方法
class Person {
// 在原型上定义生成器方法
* createNicknameIterator () {
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}
// 在类上定义生成器方法
static * createJobIterator () {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}
let jobIterator = Person.createJobIterator()
console.log(jobIterator, 'jobIterator');
console.log(jobIterator.next().value, 'jobIterator.next().value');
console.log(jobIterator.next().value, 'jobIterator.next().value');
console.log(jobIterator.next().value, 'jobIterator.next().value');
let p = new Person()
let nicknameIter = p.createNicknameIterator()
console.log(nicknameIter.next().value, 'nicknameIter.next().value');
console.log(nicknameIter.next().value, 'nicknameIter.next().value');
console.log(nicknameIter.next().value, 'nicknameIter.next().value');
添加默认的迭代器,把类实例编程可迭代对象。如下通过 增加Symbol.iterator 方法,for of 遍历时判断 p2 里面是否有Symbol.iterator 方法,有则执行。这里对 yield 的知识有点忘记了!
// 添加默认的迭代器,把类实例编程可迭代对象
class Person2 {
constructor () {
this.nicknames = ['Jack', 'Jake', 'J-Dog']
}
* [Symbol.iterator] () {
yield *this.nicknames.entries()
}
}
let p2 = new Person2()
for (let [idx, nickname] of p2) {
console.log(nickname, 'nickname');
}
可以直接返回迭代器实例
class Person3 {
constructor () {
this.nicknames = ['Jack', 'Jake', 'J-Dog']
}
[Symbol.iterator] () {
return this.nicknames.entries()
}
}
let p3 = new Person3()
for (let [idx, nickname] of p3) {
console.log(nickname, 'Person3 nickname');
}
8.4.4 继承
1. 继承基础 extends 关键字
ES6 类支持单继承,使用 extends 关键字,可以继承任何有原型和 constructor 属性的对象。可以继承一个类或者一个构造函数,如下代码中,Bug 类可以继承 Vehicle 类,Engineer 类可以继承 Person 构造函数
class Vehicle {}
class Bus extends Vehicle {}
let b = new Bus()
console.log(b instanceof Bus, 'b instanceof Bus'); // true
console.log(b instanceof Vehicle, 'b instanceof Vehicle'); // true
function Person () {}
class Engineer extends Person {}
let e = new Engineer()
console.log(e instanceof Engineer, 'e instanceof Engineer'); // true
console.log(e instanceof Person, 'e instanceof Person 构造函数'); // true
继承一个类,会继承该类上的方法和原型上的所有方法;谁调用这个方法,this 反应的就是谁(实例或者是类)
class Father {
identifyPrototype (id) {
console.log(id, this, 'identifyPrototype id this');
}
static identifyClass (id) {
console.log(id, this, 'identifyClass id this');
}
}
class Son extends Father {}
let father = new Father()
let son = new Son()
father.identifyPrototype('father')
son.identifyPrototype('son')
Father.identifyClass('Father')
Son.identifyClass('son')
如上代码中,son.identifyPrototype('son'),this 打印的是 Son 对应的实例对象;Son.identifyClass('son'),this 打印的是 Son 类
注意:extends 语法在类表达式中也可以使用
2. 构造函数 HomeObject 和 super
- super 关键字只能在派生类里面使用,使用 super 关键字可以调用父类构造函数
- 仅仅限于类构造函数、静态方法、实例方法
如下代码中,Sone 里面使用了 super 关键字,继承了父 Father 类的属性,此时 this 代表 实例对象,Father 在该实例的原型链上
class Father {
constructor () {
this.hasHomework = true
}
}
class Son extends Father {
constructor () {
super()
console.log(this instanceof Father, 'this instanceof Father'); // true
console.log(this, 'this'); // Son
}
}
new Son()
不要在 super 之前使用 this 关键字,否则会报错
class Father {
constructor () {
this.hasHomework = true
}
}
class Son extends Father {
constructor () {
+ console.log(this, 'this');
super()
console.log(this instanceof Father, 'this instanceof Father'); // true
console.log(this, 'this'); // Son
}
}
new Son()
静态方法中,可以用 super 调用继承的类上定义的静态方法。如下的 Son2 的静态方法identify 通过 super 调用了 Father 里面的 identify 方法
class Father2 {
static identify () {
console.log('Father');
}
}
class Son2 extends Father2 {
static identify () {
super.identify()
}
}
Son2.identify() // 'Father'
ES6 给构造函数和静态方法添加了内部特性 [[HomeObject]],这个特性是一个指针,指向定义该方法的对象。该指针会自动赋值,只能在 JS 引擎内部访问到,代码中我们无法访问, super 始终会定义为该指针的原型
Super 使用注意点:
- Super 只能在子类里面使用,在父类里面使用会报错
// Super 只能在子类里面使用,在父类里面使用会报错
class Father3 {
constructor () {
super()
}
}
class Son3 extends Father3 {
}
- 不能单独使用 super 关键字,要么调用构造函数,要么引用静态方法
class Father4 {
constructor () {
super
}
}
class Son4 extends Father4 {
constructor () {
super
}
}
编辑器会提示这个并爆红
- 调用 super()会调用父类构造函数,并将返回的子类实例赋值给 this。
class Father4 {
}
class Son4 extends Father4 {
constructor () {
super()
console.log(this instanceof Father4, 'this instanceof Father4'); // true
}
}
new Son4()
- super 的行为如同调用构造函数,给父类传参需要手动传入
class Father5 {
constructor (name) {
this.name = name
}
}
class Son5 extends Father5 {
constructor () {
super('xhg')
}
}
let son5 = new Son5()
console.log(son5.name, 'son5.name'); // xhg son5.name
- 如果没有定义类构造函数,在实例化子类的时候会在后台调用 super(),会把所有传给子类的参数传递给 super 关键字
class Father6 {
constructor (name, age) {
this.name = name
this.age = age
}
}
class Son6 extends Father6 {}
let son6 = new Son6('xhg', 24)
console.log(son6.name, son6.age, 'son6.name, son6.age');
上面代码等同于
class Father6 {
constructor (name, age) {
this.name = name
this.age = age
}
}
class Son6 extends Father6 {
+ constructor (name, age) {
+ super(name, age)
+ }
}
let son6 = new Son6('xhg', 24)
console.log(son6.name, son6.age, 'son6.name, son6.age');
- 在类构造函数中,不能在使用 super 之前使用 this,这个我们上面分析过
- 如果在派生类中显示调用构造函数 constructor,则要么在里面调用 super(),要么必须在其中返回一个对象
class Father7 {
constructor (name, age) {
this.name = name
this.age = age
}
}
class Son7 extends Father7 {
constructor () {
super()
}
}
class Son8 extends Father7 {
constructor () {
return {
}
}
}
let son7 = new Son7()
let son8 = new Son8()
console.log(son7, 'son7');
console.log(son8, 'son8');
3. 抽象基类
有时候可能需要定义这样一个类,他可以被继承,但是不能被实例化,借助 new.target 能够实现。如下在 new Father()的时候, new.target 指向 Father 构造函数,所以会报错
// 有时候可能需要定义这样一个类,他可以被继承,但是不能被实例化,借助 new.target 能够实现
class Father {
constructor () {
if (new.target === Father) {
throw new Error('Father cannot be directly instantiated')
}
}
}
class Son extends Father {}
new Son()
new Father()
可以利用抽象基类,提示必须要定义某个函数。注意,原型方法是在调用类构造函数之前已经存在。如下 Son3 没有定义 foo 方法
class Father2 {
constructor () {
if (new.target === Father2) {
throw new Error('Father cannot be directly instantiated')
}
if (!this.foo) {
throw new Error('Son Class must define foo')
}
}
}
class Son2 extends Father2 {
foo () {
}
}
class Son3 extends Father2 {
}
new Son2()
new Son3()
4. 继承内置类型
可以继承内置的类型如 Array
class SuperArray extends Array {
shuffle () {
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
[this[i], this[j]] = [this[j], this[i]]
}
}
}
let a = new SuperArray(1, 2, 3, 4, 5)
console.log(a instanceof SuperArray); // true
console.log(a instanceof Array); // true
5. 类混入
把不同类的行为混合到一个类中,就是类混入。如果只是要混入不同的对象,用 Object.assign 即可。只有需要混入不同类时,才需要自己去模拟这个行为
如下是最基础的混入,在 extends 后面是一个函数调用,是一个表达式;混入模式可以通过在一个表达式中连缀多个混入元素来实现。
class Vehicle {
}
function getParentClass () {
console.log('evaluated expression');
return Vehicle
}
class Bus extends getParentClass() {
}
let bus = new Bus()
console.log(bus, 'bus'); //
如果 Person 类需要和 A、B、C 组合,则需要实现 B 继承 A、C 继承 B、Person 继承 C。一种方式是定义一组可以嵌套的函数,每个函数分别接受一个超类作为参数,将混入类定义为该参数的子类。
如下代码中,将 Vehicle 作为参数传递给 BazMixin 函数,BazMixin 函数会返回一个新的类,又被传递给BarMixin 函数。最终让 Bus2 类继承这个表达式,这几个类的方法都继承到 Bus2 类上
class Vehicle2 {
}
let FooMixin = (Superclass) => class extends Superclass {
foo () {
console.log('foo');
}
}
let BarMixin = (Superclass) => class extends Superclass {
bar () {
console.log('bar');
}
}
let BazMixin = (Superclass) => class extends Superclass {
baz () {
console.log('baz');
}
}
class Bus2 extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
let b = new Bus2()
b.foo() // 'foo'
b.bar() // 'bar'
b.baz() // 'baz'
写一个辅助函数,把嵌套调用展开
function mix (BaseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => {
return current(accumulator)
}, BaseClass)
}
class Bus3 extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let bus3 = new Bus3()
console.log(bus3, 'bus3');
注意:JS 很多框架抛弃混入模式,专项复合模式。复合模式是把方法提取到独立的类和辅助对象中,然后把他们组合起来,但不使用集成。复合胜过继承。而上面的写法中,混入是把多个类的功能混到一个类。