《JavaScript高级程序设计(第4版)》
8 对象、类与面向对象编程
由于这部分在《你不知道的JavaScript(上卷)》有记过笔记,因此本篇的相关内容略写
8.1 理解对象
创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法。
8.1.1 属性的类型
1.数据属性
- configurable
- enumerable
- writable
- value
2.访问器属性
访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。在读取访问器属性时,会调用获取函数。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。
- configurable
- enumerable
- get
- set
8.1.2 定义多个属性
Object.defineProperties(要添加/修改属性的对象, 描述符对象)
描述符对象的属性与要添加/修改的属性一一对应。
8.1.3 读取属性的特性
Object.getOwnPropertyDescriptor(属性所在的对象. 要取得描述符的属性名)
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get: function() {
return this.year_;
},
set: function(newValue){
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"
8.1.4 合并对象
merge/mixin
Object.assign(一个目标对象, 一个或多个源对象)然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回 true)和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象。
Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。
dest = {};
src = { id: 'src' };
result = Object.assign(dest, src);
// Object.assign 修改目标对象
// 也会返回修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: src }
console.log(dest); // { id: src }
8.1.5 对象标识及相等判定
8.1.6 增强的对象语法
ES6 为定义和操作对象新增了很多极其有用的语法糖特性。这些特性都没有改变现有引擎的行为,但极大地提升了处理对象的方便程度。
1.属性值简写
2.可计算属性
在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性。换句话说,不能在对象字面量中直接动态命名属性。例如:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {};
person[nameKey] = 'Matt';
person[ageKey] = 27;
person[jobKey] = 'Software engineer';
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为 JavaScript 表达式而不是字符串来求值:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
[nameKey]: 'Matt',
[ageKey]: 27,
[jobKey]: 'Software engineer'
};
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
因为被当作 JavaScript 表达式求值,所以可计算属性本身可以是复杂的表达式,在实例化时再求值:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`;
}
let person = {
[getUniqueKey(nameKey)]: 'Matt',
[getUniqueKey(ageKey)]: 27,
[getUniqueKey(jobKey)]: 'Software engineer'
};
console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
3.简写方法名
8.1.7 对象解构
ES6新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。
下面的例子展示了两段等价的代码,首先是不使用对象解构的:
// 不使用对象解构
let person = {
name: 'Matt',
age: 27
};
let personName = person.name,
personAge = person.age;
console.log(personName); // Matt
console.log(personAge); // 27
然后,是使用对象解构的:
// 使用对象解构
let person = {
name: 'Matt',
age: 27
};
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
1.嵌套解构
2.部分解构
3.参数上下文匹配
8.2 创建对象
8.2.1 概述
ES5.1并没有正式支持面向对象的结构,比如类或继承。但是,正如接下来几节会介绍的,巧妙地运用原型式继承可以成功地模拟同样的行为。
ES6开始正式支持类和继承。ES6 的类旨在完全涵盖之前规范设计的基于原型的继承模式。不过,无论从哪方面看,ES6 的类都仅仅是封装了 ES5.1 构造函数加原型继承的语法糖而已。
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);
};
return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)
8.2.3 构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
与工厂模式相比,构造函数模式的区别:
- 没有显示的创建对象
- 属性和方法直接赋值给了this
- 没有return
- 按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。有助于在 ECMAScript 中区分构造函数和普通函数,ECMAScript 的构造函数就是能创建对象的函数。
- 定义自定义构造函数可以确保实例被标识为特定类型
用new创建构造函数,调用构造函数后会执行如下操作
- 在内存中创建一个新对象
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)
- 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的信对象
8.2.4 原型模式
1.理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向
原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。
2.原型层级
link
3.原型和in操作符
4.属性枚举顺序
-
for-in 循环和 Object.keys()的枚举顺序是不确定的,取决于 JavaScript 引擎,可能因浏览器而异
-
Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和 Object.assign()的枚举顺序是确定性的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。在对象字面量中定义的键以它们逗号分隔的顺序插入
8.2.5 对象迭代
1.其他原型语法
2.原型的动态性
3.原生对象原型
4.原型的问题
8.3 继承
很多面向对象语言都支持两种继承:接口继承和实现继承。
前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。
1.默认原型
2.原型与继承关系
3.关于方法
4.原型链问题
8.3.2 盗用构造函数
1.传递参数
2.盗用构造函数的问题
8.3.3 组合继承
8.3.4 原型式继承
8.3.5 寄生式继承
8.3.6 寄生式组合继承
8.4 类
ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。类(class)是ECMAScript 中新的基础性语法糖结构。虽然 ECMAScript 6 类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念
8.4.1 类定义
8.4.2 类构造函数
1.实例化
2.把类当成特殊函数
8.4.3 实例、原型和类成员
1.实例成员
2.原型方法与访问器
3.静态类方法
4.非函数原型和类成员
5.迭代器与生成器方法
8.4.4 继承
1.继承基础
2.构造函数、HomeObject和super()
3.抽象基类
4.继承内置类型
5.类混入