title: JavaScript高级程序设计第四版学习–第八章
date: 2021-5-20 21:57:39
author: Xilong88
tags: JavaScript
本章内容
理解对象
理解对象创建过程
理解继承
理解类
可能出现的面试题:
1.谈谈数据属性,访问器属性
2.定义属性,读取属性用什么方法?
3.合并属性用什么方法?
4.编程中用ES6的语法糖可以体现出水平
5.什么是对象解构
6.创建对象有几种模式?各自特点?
7.了解原型吗?
8.了解继承吗?常见模式有哪些(这里提到了7种),它们的特点是什么?
9.了解ES6的类吗?Super?
10.了解抽象基类吗?如何实现?
11.如果我们想让一个派生类有必须有某个方法,怎么实现?
12.类混入?怎么实现?
总结:这章主要讲了原型,原型链,还有多种继承方式,包括ES6的类继承,这种开发代码的模式正是面向对象语言应该有的,可以让代码优化。
应该理解每种模式以及其原理、优缺点,能够开发出更好的软件。
知识点:
1.对象有属性,有方法,对象的属性是有类型的:
数据属性和访问器属性
数据属性:
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会
写入到这个位置。数据属性有4个特性描述它们的行为。
[[Configurable]] :表示属性是否可以通过delete 删除并 重新定义,是否可以修改它的特性,以及是否可以把它改为访
问器属性。默认情况下,所有直接定义在对象上的属性的这个 特性都是true ,如前面的例子所示。[[Enumerable]]:表示属性是否可以通过for-in 循环返 回。默认情况下,所有直接定义在对象上的属性的这个特性都 是true ,如前面的例子所示。
[[Writable]] :表示属性的值是否可以被修改。默认情况 下,所有直接定义在对象上的属性的这个特性都是true ,如
前面的例子所示。
[[Value]] :包含属性实际的值。这就是前面提到的那个读取 和写入属性值的位置。这个特性的默认值为undefined
。
要修改属性的默认特性,就必须使用Object.defineProperty()
方法。这个方法接收3个参数:要给其添加属性的对象、属性的名
称和一个描述符对象。最后一个参数,即描述符对象上的属性可以
包含:configurable 、enumerable 、writable 和value ,跟
相关特性的名称一一对应。
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"
只读模式的值不能修改,非严格模式赋值被忽略,严格模式报错
configurable 设置为false,意味着不能再次调用Object.defineProperty() 修改任何非writable 属性。非严格模式下对这个属性调用delete 没有效果,严
格模式下会抛出错误。
在调用Object.defineProperty() 时,configurable
、enumerable 和writable 的值如果不指定,则都默认为false。
访问器属性:
访问器属性不包含数据值。相反,它们包含一个获取(getter)函
数和一个设置(setter)函数,不过这两个函数不是必需的。在读取
访问器属性时,会调用获取函数,这个函数的责任就是返回一个有
效的值。在写入访问器属性时,会调用设置函数并传入新值,这个
函数必须决定对数据做出什么修改。
四个属性:
[[Configurable]] :表示属性是否可以通过delete 删除并 重新定义,是否可以修改它的特性,以及是否可以把它改为数
据属性。默认情况下,所有直接定义在对象上的属性的这个特 性都是true 。
[[Enumerable]] :表示属性是否可以通过for-in
循环返 回。默认情况下,所有直接定义在对象上的属性的这个特性都 是true 。
[[Get]] :获取函数,在读取属性时调用。默认值
为undefined 。
[[Set]] :设置函数,在写入属性时调用。默认值 为undefined 。
访问器属性是不能直接定义的,必须使
用Object.defineProperty()
// 定义一个对象,包含伪私有成员year_和公共成员edition
let book = {
year_: 2017,
edition: 1
};
Object.defineProperty(book, "year", {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
});
book.year = 2018;
console.log(book.edition); // 2
year_ 中的下划线常用来表示该属性并不希望在对象方法的外部被访问。另一个属性year 被定义为一个访问器属性,其中获取函数简单地返回year_ 的值,而设置函数会做一些计算以决定正确的版本(edition)。
因此,把year 属性修改为2018会导致year_ 变成2018,edition 变成2。这是访问器属性的典型使用场景,即设置一个属性值会导致一些其他变化发生。
获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。
类似地,只有一个设置函数的属性是不能读取的,非严格模式下读取会返回undefined ,严格模式下会抛出错误。
在不支持Object.defineProperty() 的浏览器中没有办法修
改[[Configurable]] 或[[Enumerable]] 。
Object.defineProperties() :用来定义多个属性
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
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"
这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。
返回值是一个对象,对于访问器属性包含configurable 、enumerable 、get 和set 属性
对于数据属性包含configurable 、enumerable 、writable 和value 属性。
ECMAScript 2017新增了Object.getOwnPropertyDescriptors() 静态方法。这个方法实际上会在每个自有属性上调
用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;
}
}
}
});
console.log(Object.getOwnPropertyDescriptors(book));
// {
// edition: {
// configurable: false,
// enumerable: false,
// value: 1,
// writable: false
// },
// year: {
// configurable: false,
// enumerable: false,
// get: f(),
// set: f(newValue),
// },
// year_: {
// configurable: false,
// enumerable: false,
// value: 2017,
// writable: false
// }
// }
2.合并对象
Object.assign() 方法
这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()
返回true )和自有(Object.hasOwnProperty() 返回true
)属性复制到目标对象。以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]
取得属性的值,然后使用目标对象上的 [[Set]] 设置属性的值。
let dest, src, result;
/**
* 简单复制
*/
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 }
/**
* 多个源对象
*/
dest = {};
result = Object.assign(dest, { a: 'foo' }, { b: 'bar' });
console.log(result); // { a: foo, b: bar }
/**
* 获取函数与设置函数
*/
dest = {
set a(val) {
console.log(`Invoked dest setter with param ${val}`);
}
};
src = {
get a() {
console.log('Invoked src getter');
return 'foo';
}
};
Object.assign(dest, src);
// 调用src的获取方法
// 调用dest的设置方法并传入参数"foo"
// 因为这里的设置函数不执行赋值操作
// 所以实际上并没有把值转移过来
console.log(dest); // { set a(val) {...} }
Object.assign() 实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数。
let dest, src, result;
/**
* 覆盖属性
*/
dest = { id: 'dest' };
result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar' });
// Object.assign会覆盖重复的属性
console.log(result); // { id: src2, a: foo, b: bar }
// 可以通过目标对象上的设置函数观察到覆盖的过程:
dest = {
set id(x) {
console.log(x);
}
};
Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'third' });
// first
// second
// third
/**
* 对象引用
*/
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true
复制过程是不回滚的,也就是遇到错误后,之前复制的依然是复制了的,不会消失。
let dest, src, result;
/**
* 错误处理
*/
dest = {};
src = {
a: 'foo',
get b() {
// Object.assign()在调用这个获取函数时会抛出错误
throw new Error();
},
c: 'bar'
};
try {
Object.assign(dest, src);
} catch(e) {}
// Object.assign()没办法回滚已经完成的修改
// 因此在抛出错误之前,目标对象上已经完成的修改会继续存在:
console.log(dest); // { a: foo }
3.对象标识及相等判定
Object.is()
用来判断对象是否相等:
// 这些是===符合预期的情况
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false
// 这些情况在不同JavaScript引擎中表现不同,但仍被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 要确定NaN的相等性,必须使用极为讨厌的isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正确的0、-0、+0相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正确的NaN相等判定
console.log(Object.is(NaN, NaN)); // true
要检查超过两个值,递归地利用相等性传递即可:
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}
4.语法糖合集
(1)属性简写:
let name = 'Matt';
let person = {
name: name
};
console.log(person); // { name: 'Matt' }
同名属性可以简写如下:
let name = 'Matt';
let person = {
name
};
console.log(person); // { name: 'Matt' }
代码压缩程序会在不同作用域间保留属性名,以防止找不到引用。
以下面的代码为例:
function makePerson(name) {
return {
name
};
}
let person = makePerson('Matt');
console.log(person.name); // Matt
在这里,即使参数标识符只限定于函数作用域,编译器也会保留初
始的name 标识符。如果使用Google Closure编译器压缩,那么函数
参数会被缩短,而属性名不变:
function makePerson(a) {
return {
name: a
};
}
var person = makePerson("Matt");
console.log(person.name); // Matt
(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) 简写方法名
原来:
let person = {
sayName: function(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
现在:
let person = {
sayName(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
简写方法名对获取函数和设置函数也是适用的:
let person = {
name_: '',
get name() {
return this.name_;
},
set name(name) {
this.name_ = name;
},
sayName() {
console.log(`My name is ${this.name_}`);
}
};
person.name = 'Matt';
person.sayName(); // My name is Matt
简写方法名与可计算属性键相互兼容:
const methodKey = 'sayName';
let person = {
[methodKey](name) {
console.log(`My name is ${name}`);
}
}
person.sayName('Matt'); // My name is Matt
5.对象解构
对象解构就是使用与对象匹配的结
构来实现对象属性赋值。
// 不使用对象解构
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
使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时
执行多个赋值操作。如果想让变量直接使用属性的名称,那么可以使用
简写语法,比如:
let person = {
name: 'Matt',
age: 27
};
let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27
解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是undefined :
let person = {
name: 'Matt',
age: 27
};
let { name, job } = person;
console.log(name); // Matt
console.log(job); // undefined
也可以在解构赋值的同时定义默认值,这适用于前面刚提到的引用的属性不存在于源对象中的情况:
let person = {
name: 'Matt',
age: 27
};
let { name, job='Software engineer' } = person;
//假如默认值在person里存在,最后值为person里的值。
console.log(name); // Matt
console.log(job); // Software engineer
解构在内部使用函数ToObject() (不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据ToObject() 的定义),null 和
undefined 不能被解构,否则会抛出错误。
let { length } = 'foobar';
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
嵌套解构
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
let personCopy = {};
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person);
// 因为一个对象的引用被赋值给personCopy,所以修改
// person.job对象的属性也会影响personCopy
person.job.title = 'Hacker'
console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
解构赋值可以使用嵌套结构,以匹配嵌套的属性:
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
// 声明title变量并将person.job.title的值赋给它
let { job: { title } } = person;
console.log(title); // Software engineer
在外层属性没有定义的情况下不能使用嵌套解构。无论源对象还是
目标对象都一样:
let person = {
job: {
title: 'Software engineer'
}
};
let personCopy = {};
// foo在源对象上是undefined
({
foo: {
bar: personCopy.bar
}
} = person);
// TypeError: Cannot destructure property 'bar' of 'undefined' or 'null'.
// job在目标对象上是undefined
({
job: {
title: personCopy.job.title
}
} = person);
// TypeError: Cannot set property 'title' of undefined
部分解构
假如解构过程出错,就会完成部分结构:
let person = {
name: 'Matt',
age: 27
};
let personName, personBar, personAge;
try {
// person.foo是undefined,因此会抛出错误
({name: personName, foo: { bar: personBar }, age: personAge} = person);
} catch(e) {}
console.log(personName, personBar, personAge);
// Matt, undefined, undefined
参数上下文匹配
在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响arguments 对象,但可以在函数签名中声明在函数体内使用局部变量:
let person = {
name: 'Matt',
age: 27
};
function printPerson(foo, {name, age}, bar) {
console.log(arguments);
console.log(name, age);
}
function printPerson2(foo, {name: personName, age: personAge}, bar) {
console.log(arguments);
console.log(personName, personAge);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
printPerson2('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
6.创建对象
工厂模式
构造函数模式
原型模式
工厂模式:
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");
问题:没有解决对象标识问题(即新创建的对象是什么类型)
构造函数模式:
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 。
使用new 操作符方式调用构造函数会执行如下操作:
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]] 特性被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的this 被赋值为这个新对象(即this 指向新对 象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的 新对象。
,如果不想传参数,那么构造函数后面的括号可加可不加。只要有new 操作符,就可以调用相应的构造函数:
function Person() {
this.name = "Jake";
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person();
let person2 = new Person;
person1.sayName(); // Jake
person2.sayName(); // Jake
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
注意:构造函数也是函数,使用了new就是调用构造函数,不使用就是普通函数。
// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到window对象
window.sayName(); // "Greg"
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
问题:方法不能共享:
console.log(person1.sayName == person2.sayName); // false
可以通过把函数定义在构造函数之外来解决,但是这样就不能把方法聚在一起。
原型模式:
直接赋值给它们的原型,如下所示:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
使用函数表达式:
let Person = function() {};
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
总结一下:
实例有个内部属性[[prototype]],指向原型对象
构造函数有个属性prototype,指向原型对象
原型对象有个属性constructor指向构造函数
原型对象上集成了属性和方法
原型对象也有个内部属性[[prototype]]指向另一个原型对象
也就是说,原型对象是另一个构造函数的实例
这样一层一层形成的链就叫做原型链
可以看看代码:
/**
* 构造函数可以是函数表达式
* 也可以是函数声明,因此以下两种形式都可以:
* 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
console.log(Person.prototype.__proto__);
// {
// constructor: f Object(),
// toString: ...
// hasOwnProperty: ...
// isPrototypeOf: ...
// ...
// }
let person1 = new Person(),
person2 = new Person();
/**
* 构造函数、原型对象和实例
* 是3个完全不同的对象:
*/
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
/**
* 实例通过__proto__链接到原型对象,
* 它实际上指向隐藏特性[[Prototype]]
*
* 构造函数通过prototype属性链接到原型对象
*
* 实例与构造函数没有直接联系,与原型对象有直接联系
*/
console.log(person1.__proto__ === Person.prototype); // true
conosle.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
实例和构造函数没有直接关系
ECMAScript的Object 类型有一个方法叫
Object.getPrototypeOf() ,返回参数的内部特
性[[Prototype]] 的值。例如:
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // "Nicholas"
Object 类型还有一个setPrototypeOf() 方法,可以向实例的私
有特性[[Prototype]] 写入一个新值。这样就可以重写一个对象
的原型继承关系:
let biped = {
numLegs: 2
};
let person = {
name: 'Matt'
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
使用Object.setPrototypeOf() 可能造成的性能下降,可
以通过Object.create() 来创建一个新对象,同时为其指定原型:
let biped = {
numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
当给对象添加属性与原型的属性同名,会覆盖原型的属性,也就是说,在对象上找到了属性就不在原型上找了。原型上属性没有消失。
如果想重新去原型上找,不能给属性赋值为null,应该删除属性,delete
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
console.log(person2.name); // "Nicholas",来自原型
delete person1.name;
console.log(person1.name); // "Nicholas",来自原型
hasOwnProperty() 方法用于确定某个属性是在实例上还是在原型
对象上
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
console.log(person1.hasOwnProperty("name")); // false
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
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
Object.getOwnPropertyDescriptor() 方法只对实例属性
有效。要取得原型属性的描述符,就必须直接在原型对象上调
用Object.getOwnPropertyDescriptor() 。
in 操作符会在可以通过对象访问指定属性时返回
true ,无论该属性是在实例上还是在原型上。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
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 = "Greg";
console.log(person1.name); // "Greg",来自实例
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
如果要确定某个属性是否存在于原型上,则可以
像下面这样同时使用hasOwnProperty() 和in 操作符:
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
在for-in 循环中使用in 操作符时,可以通过对象访问且可以被枚
举的属性都会返回,包括实例属性和原型属性
要获得对象上所有可枚举的实例属性,可以使用Object.keys()
方法。这个方法接收一个对象作为参数,返回包含该对象所有可枚
举属性名称的字符串数组。比如:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let keys = Object.keys(Person.prototype);
console.log(keys); // "name,age,job,sayName"
let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name,age]"
如果想列出所有实例属性,无论是否可以枚举,都可以使用Object.getOwnPropertyNames() :
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "[constructor,name,age,job,sayName]"
注意,返回的结果中包含了一个不可枚举的属性constructor。Object.keys() 和Object.getOwnPropertyNames() 在适当的时候都可用来代替for-in 循环。
for in 和 Object.keys()一样返回可列举的属性,但是后者返回的是数组,前者一项一项返回。
Object.getOwnPropertySymbols() 方法
let k1 = Symbol('k1'),
k2 = Symbol('k2');
let o = {
[k1]: 'k1',
[k2]: 'k2'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]
针对符号属性进行返回
属性枚举顺序:
for-in 循环、Object.keys()
、Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols() 以及Object.assign()
在属性枚举顺序方面有很大区别
for-in 循环和Object.keys() 的枚举顺序是不确定的,取决于JavaScript引擎,可能因浏览器而 异。
Object.getOwnPropertyNames() 、Object.getOwnPropertySymbols()
和Object.assign() 的 枚举顺序是确定性的。先以升序枚举数值键,然后以插入顺序枚举
字符串和符号键。在对象字面量中定义的键以它们逗号分隔的顺序 插入。
let k1 = Symbol('k1'),
k2 = Symbol('k2');
let o = {
1: 1,
first: 'first',
[k1]: 'sym2',
second: 'second',
0: 0
};
o[k2] = 'sym2';
o[3] = 3;
o.third = 'third';
o[2] = 2;
console.log(Object.getOwnPropertyNames(o));
// ["0", "1", "2", "3", "first", "second", "third"]
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]
对象迭代:
ECMAScript 2017新增了两个静态方法,用于将对象内容转换为序列化
的——更重要的是可迭代的——格式。这两个静态方法
Object.values() 和Object.entries() 接收一个对象,返回它们内
容的数组。Object.values() 返回对象值的数
组,Object.entries() 返回键/值对的数组。
const o = {
foo: 'bar',
baz: 1,
qux: {}
};
console.log(Object.values(o));
// ["bar", 1, {}]
console.log(Object.entries((o)));
// [["foo", "bar"], ["baz", 1], ["qux", {}]]
注意,非字符串属性会被转换为字符串输出。另外,这两个方法执行对象的浅复制:
const o = {
qux: {}
};
console.log(Object.values(o)[0] === o.qux);
// true
console.log(Object.entries(o)[0][1] === o.qux);
// true
符号属性会被忽略:
const sym = Symbol();
const o = {
[sym]: 'foo'
};
console.log(Object.values(o));
// []
console.log(Object.entries((o)));
// []
原型模式写法:
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
// 恢复constructor属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
这样做
第一点是为了把属性封装在一起
第二点是要给原型赋值构造函数
第三点是构造函数应该改成不可枚举状态
原生对象原型
原型是实现所有原生引用类型的模式。所有原生引用类型的构造函数(包括Object 、Array 、String 等)都在原型上定义了实例方法。
通过原生对象的原型可以取得所有默认方法的引用,也可以给原生
类型的实例定义新的方法。
String.prototype.startsWith = function (text) {
return this.indexOf(text) === 0;
};
let msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true
尽管可以这么做,但并不推荐在产品环境中修改原生
对象原型。这样做很可能造成误会,而且可能引发命名冲突
(比如一个名称在某个浏览器实现中不存在,在另一个实现中
却存在)。另外还有可能意外重写原生的方法。推荐的做法是
创建一个自定义的类,继承原生类型。
原型的问题:
1.它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。
2.原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性也还好,如前面例子中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。来看下面的例子:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends: ["Shelby", "Court"],
sayName() {
console.log(this.name);
}
};
let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true
也就是引用类型的属性,共享的同一个实例,这样的话修改其中一个,所有实例的这个引用值都会变。
7.继承
原型链
盗用构造函数
组合继承
原型式继承
寄生式继承
寄生式组合继承
ES6类继承
原型链:
每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。
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
实际上,原型链中还有一环。默认情况下,所有引用类型都继承自Object ,这也是通过原型链实现的:
原型与继承关系
第一种方式是使用instanceof 操作符,如果一个实例的原型链中出现过相应的构造函数,则instanceof 返回true
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
第二种方式是使用isPrototypeOf() 方法。原型
链中的每个原型都可以调用这个方法,如下例所示,只要原型链中
包含这个原型,这个方法就返回true :
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
原型链的问题:
原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。下面的例子揭示了这个问题:
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green,black"
也就是说,继承SuperType时,new出来的对象是同一个对象,后面的子对象都使用相同的引用。
第二个问题:子类型在实例化时不能给父类型的构造函数传参。事实上,我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。
就是说上面的new SuperType()传入了参数就会改变后面所有子类实例。
盗用构造函数:
也叫做“对象伪装”或“经典继承”
基本思路很简单:在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply() 和call() 方法以新创建的对象为上下文执行构造函数。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 继承SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
这样的话,相当于每次创建一个子类的实例,都会调用父类的构造函数构造一个新的父类对象,这样就可以不共享属性指针了。
传递参数:
相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造
函数中向父类构造函数传参。
function SuperType(name){
this.name = name;
}
function SubType() {
// 继承SuperType并传参
SuperType.call(this, "Nicholas");
// 实例属性
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // "Nicholas";
console.log(instance.age); // 29
盗用构造函数的问题:
盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。
也就是SuperType的prototype上假如定义方法,不会被继承。
组合继承:
也叫伪经典继承
综合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
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", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
组合继承弥补了原型链和盗用构造函数的不足,是JavaScript中使用最多的继承模式。而且组合继承也保留了instanceof
问题:组合继承存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
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 object(o) {
function F() {}
F.prototype = o;
return new F();
}
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
原型式继承适用于这种情况:你有一个对象,想在它的基础上再创建一个新对象。你需要把这个对象先传给object() ,然后再对返回的对象进行适当修改。
ECMAScript 5通过增加Object.create() 方法将原型式继承的概念规
范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对
象定义额外属性的对象(第二个可选):
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
Object.create() 的第二个参数与Object.defineProperties() 的
第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方
式添加的属性会遮蔽原型对象上的同名属性。
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
console.log(anotherPerson.name); // "Greg"
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型链是一样的。
寄生式继承
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场
景。object() 函数不是寄生式继承所必需的,任何返回新对象的函数
都可以在这里使用。
问题: 通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
寄生式组合继承
寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。
基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。
说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
//给返回的prototype 对象设置constructor 属性,解决由于重写原型导致默认constructor
//丢失的问题。
subType.prototype = prototype; // 赋值对象
}
实例:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
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);
};
这里只调用了一次SuperType 构造函数,避免了SubType.prototype
上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原
型链仍然保持不变,因此instanceof 操作符和isPrototypeOf() 方
法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。
也就是说这种方式不会调用两次父类的构造函数,实际上之前的组合集成调用了两次,第二次实例才把子类的属性覆盖,以寄生式组合就不会给子类赋值了,这样就可以提高效率。
ES6 类继承:
ECMAScript 6新引入的class 关键字具有正式定义类的能力。类(class)是ECMAScript中新的基础性语法糖结构
// 类声明
class Person {}
// 类表达式
const Animal = class {};
函数声明可以提升,但类定义不能:
console.log(FunctionExpression); // undefined
var FunctionExpression = function() {};
console.log(FunctionExpression); // function() {}
console.log(FunctionDeclaration); // FunctionDeclaration() {}
function FunctionDeclaration() {}
console.log(FunctionDeclaration); // FunctionDeclaration() {}
console.log(ClassExpression); // undefined
var ClassExpression = class {};
console.log(ClassExpression); // class {}
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
class ClassDeclaration {}
console.log(ClassDeclaration); // class ClassDeclaration {}
另一个跟函数声明不同的地方是,函数受函数作用域限制,而类受块作
用域限制:
{
function FunctionDeclaration() {}
class ClassDeclaration {}
}
console.log(FunctionDeclaration); // FunctionDeclaration() {}
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方
法,但这些都不是必需的。空的类定义照样有效。
默认情况下,类定义中的代码都在严格模式下执行。
// 空类定义,有效
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); // ReferenceError: PersonName is not defined
使用new调用类的构造函数会执行如下操作。
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]] 指针被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的this 被赋值为这个新对象(即this 指向新对 象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创 建的新对象。
class Animal {}
class Person {
constructor() {
console.log('person ctor');
}
}
class Vegetable {
constructor() {
this.color = 'orange';
}
}
let a = new Animal();
let p = new Person(); // person ctor
let v = new Vegetable();
console.log(v.color); // orange
类实例化时传入的参数会用作构造函数的参数。如果不需要参数,
则类名后面的括号也是可选的:
class Person {
constructor(name) {
console.log(arguments.length);
this.name = name || null;
}
}
let p1 = new Person; // 0
console.log(p1.name); // null
let p2 = new Person(); // 0
console.log(p2.name); // null
let p3 = new Person('Jake'); // 1
console.log(p3.name); // Jake
默认情况下,类构造函数会在执行之后返回this 对象。构造函数
返回的对象会被用作实例化的对象,如果没有什么引用新创建的
this 对象,那么这个对象会被销毁。不过,如果返回的不是this
对象,而是其他对象,那么这个对象不会通过instanceof 操作符
检测出跟类有关联,因为这个对象的原型指针并没有被修改。
class Person {
constructor(override) {
this.foo = 'foo';
if (override) {
return {
bar: 'bar'
};
}
}
}
let p1 = new Person(),
p2 = new Person(true);
console.log(p1); // Person{ foo: 'foo' }
console.log(p1 instanceof Person); // true
console.log(p2); // { bar: 'bar' }
console.log(p2 instanceof Person); // false
类构造函数与构造函数的主要区别是,调用类构造函数必须使用new 操作符。而普通构造函数如果不使用new 调用,那么就会以全局的this (通常是window )作为内部对象
调用类构造函数时如果忘了使用new 则会抛出错误
类构造函数没有什么特殊之处,实例化之后,它会成为普通的实例
方法(但作为类构造函数,仍然要使用new 调用)。因此,实例化
之后可以在实例上引用它:
class Person {}
// 使用类创建一个新实例
let p1 = new Person();
p1.constructor();
// TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor();
类可以看做普通函数,重点在于,类中定义的constructor 方法不会 被当成构造函数,在对它使用instanceof 操作符时会返回false 。
如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么instanceof 操
作符的返回值会返回true
class Person {}
let p1 = new Person();
console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Person.constructor); // false
let p2 = new Person.constructor();
console.log(p2.constructor === Person); // false
console.log(p2 instanceof Person); // false
console.log(p2 instanceof Person.constructor); // true
类也可以当成参数传递
// 类可以像函数一样在任何地方定义,比如在数组中
let classList = [
class {
constructor(id) {
this.id_ = id;
console.log(`instance ${this.id_}`);
}
}
];
function createInstance(classDefinition, id) {
return new classDefinition(id);
}
let foo = createInstance(classList[0], 3141); // instance 3141
与立即调用函数表达式相似,类也可以立即实例化:
// 因为是一个类表达式,所以类名是可选的
let p = new class Foo {
constructor(x) {
console.log(x);
}
}('bar'); // bar
console.log(p); // Foo {}
实例成员
每次new,都是一个实例成员
class Person {
constructor() {
// 这个例子先使用对象包装类型定义一个字符串
// 为的是在下面测试两个对象的相等性
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
this.nicknames = ['Jake', 'J-Dog']
}
}
let p1 = new Person(),
p2 = new Person();
p1.sayName(); // Jack
p2.sayName(); // Jack
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); // false
p1.name = p1.nicknames[0];
p2.name = p2.nicknames[1];
p1.sayName(); // Jake
p2.sayName(); // J-Dog
原型方法与访问器
为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。
class Person {
constructor() {
// 添加到this的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance');
}
// 在类块中定义的所有内容都会定义在类的原型上
locate() {
console.log('prototype');
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
可以把方法定义在类构造函数中或者类块中,但不能在类块中给原
型添加原始值或对象作为成员数据:
class Person {
name: 'Jake'
}
// Uncaught SyntaxError: Unexpected token
类方法等同于对象属性,因此可以使用字符串、符号或计算的值作
为键:
const symbolKey = Symbol('symbolKey');
class Person {
stringKey() {
console.log('invoked stringKey');
}
[symbolKey]() {
console.log('invoked symbolKey');
}
['computed' + 'Key']() {
console.log('invoked computedKey');
}
}
let p = new Person();
p.stringKey(); // invoked stringKey
p[symbolKey](); // invoked symbolKey
p.computedKey(); // invoked computedKey
类定义也支持获取和设置访问器。语法与行为跟普通对象一样:
class Person {
set name(newName) {
this.name_ = newName;
}
get name() {
return this.name_;
}
}
let p = new Person();
p.name = 'Jake';
console.log(p.name); // Jake
静态类方法
和java一样,就是方法名前面加static,然后可以直接通过类名访问
class Person {
constructor() {
// 添加到this的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, {constructor: ... }
Person.locate(); // class, class Person {}
静态类方法非常适合作为实例工厂:
class Person {
constructor(age) {
this.age_ = age;
}
sayAge() {
console.log(this.age_);
}
static create() {
// 使用随机年龄创建并返回一个Person实例
return new Person(Math.floor(Math.random()*100));
}
}
console.log(Person.create()); // Person { age_: ... }
非函数原型和类成员
虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加:
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
// 在类上定义数据成员
Person.greeting = 'My name is';
// 在原型上定义数据成员
Person.prototype.name = 'Jake';
let p = new Person();
p.sayName(); // My name is Jake
类定义中之所以没有显式支持添加数据成员,是因为在共享目标(原型和类)上添加可变(可修改)数据成员是一种反模式。一般来说,对象实例应该独自拥有通过this 引用的数据。
迭代器与生成器方法
类定义语法支持在原型和类本身上定义生成器方法:
class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}
// 在类上定义生成器方法
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog
因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类
实例变成可迭代对象:
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
*[Symbol.iterator]() {
yield *this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog
也可以只返回迭代器实例:
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
[Symbol.iterator]() {
return this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog
继承:
ECMAScript6新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链。
ES6类支持单继承。使用extends 关键字,就可以继承任何拥
有[[Construct]] 和原型的对象。很大程度上,这意味着不仅可
以继承一个类,也可以继承普通的构造函数(保持向后兼容):
class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
function Person() {}
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true
派生类都会通过原型链访问到类和原型上定义的方法。this 的值
会反映调用相应方法的实例或者类:
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 {}
Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}
注意 extends 关键字也可以在类表达式中使用,因此let
Bar = class extends Foo {} 是有效的语法。
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
注意 ES6给类构造函数和静态方法添加了内部特
性[[HomeObject]] ,这个特性是一个指针,指向定义该方法
的对象。这个指针是自动赋值的,而且只能在JavaScript引擎内
部访问。super 始终会定义为[[HomeObject]] 的原型。
注意:
super 只能在派生类构造函数和静态方法中使用。
不能单独引用super 关键字,要么用它调用构造函数,要么用
它引用静态方法。
调用super() 会调用父类构造函数,并将返回的实例赋值给
this 。
super() 的行为如同调用构造函数,如果需要给父类构造函数
传参,则需要手动传入。
如果没有定义类构造函数,在实例化派生类时会调用super()
,而且会传入所有传给派生类的参数。
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
在类构造函数中,不能在调用super() 之前引用this 。
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(this);
}
}
new Bus();
// 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()); // {}
抽象基类:
有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化。
通过new.target可以实现:
// 抽象基类
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 {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。
// 抽象基类
class Vehicle {
constructor() {
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
if (!this.foo) {
throw new Error('Inheriting class must define foo()');
}
console.log('success!');
}
}
// 派生类
class Bus extends Vehicle {
foo() {}
}
// 派生类
class Van extends Vehicle {}
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()
继承内置类型
ES6类为继承内置引用类型提供了顺畅的机制,开发者可以方便地
扩展内置类型:
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 Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a); // [3, 1, 4, 5, 2]
有些内置类型的方法会返回新实例。默认情况下,返回实例的类型
与原始实例的类型是一致的:
class SuperArray extends Array {}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2))
console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // true
如果想覆盖这个默认行为,则可以覆盖Symbol.species 访问器,
这个访问器决定在创建返回的实例时使用的类:
class SuperArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2))
console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // false
类混入
就是把多个类混合为一个类:
class Vehicle {}
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 Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
通过写一个辅助函数,可以把嵌套调用展开:
class Vehicle {}
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');
}
};
function mix(BaseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
很多JavaScript框架(特别是React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)
组合胜过继承(composition over inheritance)