理解对象
创建自定义对象的通常方式是创建 Object 的一个新实例
然后再给它添加属性和方法
let person1 = new Object();
person1.name = "Nicholas";
person1.age = 29;
person1.job = "Software Engineer";
person1.sayName = function() {
console.log(this.name);
}
// 与上面的写法相等
let person2 = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName(){
console.log(this.name);
}
};
person1 与 person2 对象是等价的,属性和方法都一样(都有自己的特征)
属性和类型
ECMA-262 使用一些内部特征来描述属性的特征
开发者不能在 Javascript 中直接访问这些特性
为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来[Enumerable]
属性分为两种:数据属性和访问器属性
数据属性
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置
数据属性由4个特性描述它们的行为
属性 | 描述 |
---|---|
[Configurable] | 表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true |
[Enumerable] | 表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true |
[Writable] | 表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true |
[Value] | 包含属性的实际的值。这就是前面提到的读取和写入属性的位置,这个特性的默认值为 undefined |
Object.defineProperty
writable
let person1 = {
name: "Nicholas"
}
// person1 创建了一个名为 name 的属性,并给它赋予一个值 "Nicholas"
// - 意味着 [[Value]] 特性会被设置为 "Nicholas", 之后对这个值的任何修改都会保存这个设置
// 要修改属性的默认特性,就必须使用 Object.defineProperty() 方法
// - 方法接收 3 个参数
// - 要给其添加属性的对象
// - 属性的名称和一个描述符对象
// - 描述符对象上的属性:可以包含(configurable, enumerable, writable, value)
let person2 = {};
Object.defineProperty(person2, "name", {
writable: false,
value: "Nicholas"
})
// writable为 false,代表这个属性的值就不能再修改了
// 非严格模式下尝试给这个属性重新赋值会被忽略
// 严格模式下尝试修改只读属性的值会抛出错误
console.log(person2.name); // Nicholas
person2.name = "Greg";
console.log(person2.name); // Nicholas
configurable
let person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Nicholas"
})
console.log(person.name);
delete person.name;
console.log(person.name);
// configurable 设置为 false
// 意味着这个属性不能从对象上删除
// 非严格模式下对这个属性调用 delete 没有效果,严格模式下会抛出错误。
// 一个属性被定义为不可配置之后,就不能再变回可配置的了,再次调用 Object.defineProperty() 并修改任何非 writable 属性会导致错误
// 可以对同一个属性多次调用 Object.defineProperty(),但在把 conconfigurable 设置为 false 之后就会受限制了
访问器属性 Object.defineProperty()
访问器属性不包含数据值,它们包含获取(getter) 函数和一个设置 (setter) 函数,不过这两个函数不是必须的
- 读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值
- 写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改
访问器属性有 4 个特性描述它们的行为
属性 | 描述 |
---|---|
[Configurable] | 表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true |
[Enumerable] | 表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true |
[Get] | 获取函数,在读取属性时调用,默认值为 undefined |
[Set] | 设置函数,在写入属性时调用,默认值为 undefined |
访问器属性不能直接定义,必须使用 Object.defineProperty()
// 访问器属性不能直接定义,必须使用 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
console.log(book); // {year_: 2018, edition: 2}
// 获取函数和设置函数不一定都要定义
// 只定义获取函数意味着属性是只读的 尝试修改属性会被忽略。
// 在严格模式下,尝试写入只定义了获取函数的属性会抛出错误
// 只有一个设置函数的属性是不能读取的
// 非严格模式下读取会返回 undefined,严格模式下会抛出错误
定义多个属性 Object.defineProperties()
在一个对象上同时定义多个属性的可能性是非常大的
ECMAScript 提供了 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;
}
}
}
})
console.log(book);
读取属性的特征
Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor() 方法可以取得指定的属性的属性描述符
接收两个参数
- 属性所在的对象
- 要取得其描述符的属性名
- 返回是一个对象
对于访问器属性包含 configurable,enumerable,get 和 set 属性,对于数据属性包含 configurable,enumerable,writable 和 value 属性
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 descriptor1 = Object.getOwnPropertyDescriptor(book, 'year_');
console.log(descriptor1.value); // 2017
console.log(descriptor1.configurable) // false
console.log(descriptor1.get); // "undefined"
console.log('====================================================================')
let descriptor2 = Object.getOwnPropertyDescriptor(book, 'year');
console.log(descriptor2.value); // "undefined"
console.log(descriptor2.configurable) // false
// get 是一个指向获取函数的指针
console.log(descriptor2.get); // "function (){ return this.year_; }"
Object.getOwnPropertyDescriptors() 静态方法
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));
// {year_: {…}, edition: {…}, year: {…}}
合并对象
Object.assign()
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);
console.log(dest);
多个源对象
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);
// 调用 scr 的获取方法
// 调用 dest 的设置方法并传入参数 foo
// 因为这里的设置函数不执行赋值操作
// 所以实际上并没有把值转移过来
console.log(dest)
Object.assign() 对每个源对象执行的是浅复制
如果多个源对象都有相同的属性,则使用最后一个复制的值。
从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象
(不能在两个对象间转移获取函数和设置函数)
覆盖属性
let dest, src, result;
dest = {id: 'dest'};
result = Object.assign(dest, {id: 'scr1', a:'foo'}, {id:'src2', b:'bar'});
// Object.assign 会覆盖重复的属性
console.log(result);
// 可以通过目标对象上的设置函数观察到覆盖的过程
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
如果赋值期间出错,则操作会中止并退出,同时抛出错误
Object.assign() 没有 “回滚” 之前赋值的概念,因此它是一个尽力而为,可能只会完成部分的复制方法
let dest, src, result;
// 错误处理
dest = {};
src = {
a: 'foo',
get b(){
// Object.assign() 在调用这个获取函数时会抛出错误
throw new Error();
},
c: 'car'
}
try{
Object.assign(dest, src);
}catch(e){};
// Object.assign() 没办法回滚已经完成的修改
console.log(dest); // {a: 'foo'}
对象标识及相等判定 Object.is()
// 这个方法接收两个参数
Object.is(true, 1); // false
Object.is({}, {}); // false
Object.is("2", 2); // false
// 正确的 0, -0, +0 相等/不等判定
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
// 正确的 NaN 相等判定
console.log('Object.is(NaN, NaN): ', Object.is(NaN, NaN)); // true
// 要检查超过两个值,递归地利用相等性传递即可
function recureivelyCheakEqual(x, ...rest){
return Object.is(x, rest[0]) && (rest.length < 2 || recureivelyCheakEqual(...rest));
}
增强的对象语法
属性值简写
// 在给对象添加变量的时候,经常会发现属性名和变量名是一样的
let name = "Matt";
let person = {
name:name
};
console.log('person: ', person) // {name: 'Matt'}
// 简写属性名只要使用变量名(不用再写冒号)就会自动被解释为同名的属性键(没有找到同名的变量,抛出错误 ReferenceError)
let name1 = "Matt";
let person1 = {
name1
};
console.log('person1: ', person1) // {name1: 'Matt'}
// 代码压缩程序会再不同作用域间保留属性名,以防止找不到引用
function makePerson(name){
return {
name
};
};
var person3 = makePerson("Matt");
console.log('person3: ', person3) // {name: 'Matt'}
可计算属性
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {};
person[nameKey] = "Matt";
person[ageKey] = 27;
person[jobKey] = "Software enginner";
console.log('person: ', person);
console.log('====================')
// 使用对象字面量 ↓
const nameKey1 = 'name';
const ageKey1 = 'age';
const jobKey1 = 'job';
let person1 = {
[nameKey1]: "Matt",
[ageKey1]: 27,
[jobKey1]: "Software enginner"
};
console.log('person1: ', person1);
console.log('====================')
// 因为被当作 JavaScript 表达式的求值,所以可计算属性本身可以是很复杂的表达式
const nameKey2 = 'name';
const ageKey2 = 'age';
const jobKey2 = 'job';
let uniqueToken = 0
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`
}
let person2 = {
[getUniqueKey(nameKey2)]: "Matt",
[getUniqueKey(ageKey2)]: 27,
[getUniqueKey(jobKey2)]: "Software engineer"
}
console.log('person2: ', person2)
console.log('====================')
可计算属性表达式中抛出任何错误都会中断对象创建
如果表达式抛出错误,那么之前完成的计算是不能回滚的
简写方法名
在给对象定义方法时,通常都要写一个方法名,冒号,然后再引用一个匿名函数表达式
let person = {
sayName: function (name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt') // My name is Matt
新的简写方法语法遵循同样的模式,要放弃给函数表达式命名
let person = {
name_: '',
get name(){
return this.name_;
},
set name(value){
this.name_ = value;
},
sayName(){
console.log(`My name is ${this.name_}`)
}
};
person.name = "Matt";
person.sayName();
简写方法名与可计算属性键相互兼容
const methodKey = 'sayName';
let person = {
[methodKey] (name){
console.log(`My name is ${name}`)
}
}
对象解构
ES6 新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作(对象解构就是使用与对象匹配的解构来实现对象属性赋能)
let person = {
name: "Matt",
age: 27
};
// 不使用对象解构
let personName = person.name;
let personAge = person.age;
console.log('personName: ', personName, 'personAge: ', personAge)
// 使用对象解构
let {name: personName1, age: personAge1} = person;
console.log('personName1: ', personName1, 'personAge1: ', personAge1)
// 使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作(让变量直接使用属性的名称)
let {name, age} = person;
console.log('name: ', name, 'age: ', age)
// 解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是 undefined
let {name1, job1} = person;
console.log('name1: ', name1, 'job1: ', job1)
// 可在解构赋值的同时定义默认值。(适用于引用属性不存在于源对象中的情况)
let {name2 = 'name2', job2 = 'job2'} = person;
console.log('name2: ', name2, 'job2: ', job2)
// null 和 undefined 不能被解构,否则会抛出错误
let {length} = "foobar";
console.log('let {length} = "foobar": ', length)
let {constructor: c} = 4;
console.log(c === (4).constructor)
console.log(c === Number)
// 解构不要求变量必须在解构表达式中声明(如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中)
let personName2, personAge2;
let person2 = {
name: "Matt",
age: 22
};
({name: personName2, age: personAge2} = person)
console.log('personName2, personAge2: ', personName2, personAge2)
嵌套解构
解构对于引用嵌套的属性或赋值目标没有限制(可以通过解构来复制对象属性)
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: ', person)
console.log('personCopy: ', personCopy)
解构赋值可以使用嵌套解构,以匹配嵌套的属性:
let person = {
name: "Matt",
age: 27,
job: {
title: "Software engineer"
}
}
// 声明 title 变量并将 person.job.title 的值赋给它
let {job: {title}} = person;
console.log(title)
在外层属性没有定义的情况下不能使用嵌套解构(无论对象还是目标对象都一样)
let person = {
job: {
title: "Software engineer"
}
}
console.log(person);
let personCopy = {};
// foo 在源对象上是 undefined
({
foo: {
bar: personCopy.bar
}
} = person)
// 报错 TypeError: Cannot read properties of undefined (reading 'bar')
部分解构
涉及多个属性的解构赋值时一个输出无关顺序化的操作
如果一个解构表达式涉及多个赋值,开始赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分
let person = {
name: "Matt",
age: 27
};
let personName, personBar, personAge;
try {
// person.foo 是 undefined 因此会抛出错误
({name: personName, foo: {bar: personBar}, age: personAge} = person);
} catch { }
console.log(personName, personBar, personAge)
参数上下文匹配
在函数参数列表中也可以进行解构赋值
对参数的解构赋值不会影响 arguments 对象
可以在函数签名中声明在函数体内使用局部变量
let person = {
name: "Matt",
age: 27
}
function printPerson(foo, {name, age}, bar) {
console.log('printPerson: ', arguments);
console.log('printPerson: ', name, age);
console.log('printPerson ==========================')
}
function printPerson2(foo, {name: personName, age: personAge}, bar) {
console.log('printPerson2: ', arguments)
console.log('printPerson2: ', personName, personAge)
console.log('printPerson2 ==========================')
}
printPerson('lst', person, '2nd')
printPerson2('lst', {name: 'Matt', age: 27}, '2nd')
创建对象
使用 Object 构造函数或对象字面量可以方便地创建对象,但这种方式也有明显不足
- 创建具有同样接口的多个对象需要重复编写很多代码
巧妙地运用原型式继承可以成功地模拟同样的行为
工厂模式
用于抽象创建特定对象的过程
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");
console.log('person1: ', person1)
console.log('person2: ', person2)
构造函数模式
可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法
/*
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;
}
*/
// 改写
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()
person2.sayName()
Person() 函数替代了 createPerson() 工厂函数
Person() 内部的代码跟 createPerson() 基本是一样的
只是有如下区别
- 没有显式地创建对象
- 属性和方法直接赋值给了 this
- 没有 return
Person() 的首字母大写了(按照惯例,构造函数的首字母都是要大写的,非构造函数则以小写字母开头。有助于在 ECMAScript 中区分构造函数和普通函数)
要创建 构造函数 的实例,应使用 new 操作符。
以这种方式调用构造函数会执行如下操作
- 在内存中创建一个新对象
- 这个新对象内部的 [ [prototype] ] 特性被赋值为构造函数的 prototype 属性
- 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
- 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
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");
// 这两个对象都有一个 constructor 属性指向 Person
console.log('person1.constructor === Person: ', person1.constructor === Person)
console.log('person2.constructor === Person: ', person2.constructor === Person)
console.log('======================================================================')
// constructor 是用于表示对象类型的
// 一般认为 instanceof 操作符是确定对象类型更可靠的方式
// 调用 instanceof 操作符
console.log('person1 instanceof Person: ', person1 instanceof Person)
console.log('person2 instanceof Person: ', person2 instanceof Person)
console.log('======================================================================')
// person1, person2 也被认为是 Object 的实例(因为所有自定义对象都继承自 Object)
构造函数不一定要要写成函数声明的形式,赋值给变量的函数表达式也可以是表示构造函数
// 构造函数不一定要要写成函数声明的形式,赋值给变量的函数表达式也可以是表示构造函数
let Person = function (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name)
};
}
在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加,只要有 new 操作符,就可以调用相应的构造函数
let Person = function (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name)
};
}
// 在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加,只要有 new 操作符,就可以调用相应的构造函数
let person3 = new Person();
person3.sayName();
console.log('======================================================================')
构造函数也是函数
构造函数和普通函数的唯一的区别就是调用方式不同
任何函数只要使用 new 操作符调用就是构造函数
不使用 new 操作符调用的函数就是普通函数
// 构造函数不一定要要写成函数声明的形式,赋值给变量的函数表达式也可以是表示构造函数
let Person = function (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name)
};
}
// 作为构造函数
let person = new Person('Nicholas', 29, 'Software Engineer');
person.sayName();
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到 window 对象
window.sayName();
// 在另外一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName();
构造函数的问题
构造函数的主要问题在于,其定义的方法会在每个实例都创建一遍。
/*
let Person = function (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name)
};
}
*/
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)"); // 逻辑等价
// 以这种方式创建函数会带来不同的作用域链和标识符解析
// 但创建新 Function 实例的机制是一样的
// 因此不同实例上的函数虽然同名却不相等
}
let person1 = new Person()
let person2 = new Person()
console.log(person1.sayName === person2.sayName)
// 因为都是做一样的事,所以没必要定义两个不同的 Function 实例
// 况且 this 对象可以把函数与对象的绑定推迟运行时
function Person1(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName; // 逻辑等价
// 以这种方式创建函数会带来不同的作用域链和标识符解析
// 但创建新 Function 实例的机制是一样的
// 因此不同实例上的函数虽然同名却不相等
}
function sayName(){
console.log(this.name);
}
let person3 = new Person("Nicholas", 29, "Software Engineer");
let person4 = new Person("Greg", 27, "Doctor");
person3.sayName()
person4.sayName()
原型模式
每个函数都会创建 prototype 属性
这个属性是一个对象(包含应该由特定引用类型的实例共享的属性和方法)
实际上 :
- 这个对象就是通过调用构造函数创建的对象的原型
- 使用原型对象的好处是在它上面定义的属性和方法都可以被对象共享
- 原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型
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()
let person2 = new Person();
person2.sayName()
console.log(person1.sayName === person2.sayName);
使用函数表达式也可以
// 定义的方式改为函数表达式
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()
let person2 = new Person();
person2.sayName()
console.log(person1.sayName === person2.sayName);
理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)
所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。
自定义构造函数时,原型对象默认只会获得 constructor 属性,其他所有方法都继承自 Object
每次调用构造函数创建一个新实例,这个实例内部 [ [ Prototype ] ] 指针就会被赋值为构造函数的原型对象(脚本中没有访问这个 [ [Prototype ] ] 特性的标准方式,但浏览器会在每个对象上暴露 __proto__ 属性,通过这个属性可以访问对象的原型
实例与构造函数原型之间有联系。但实例与构造函数之间没有
// 构造函数可以是函数标傲世,也可以时函数声明
function Person(){}
// 声明之后,构造函数就有一个与之关联的原型对象
console.log(typeof Person.prototype);
console.log(Person.prototype);
console.log('==============================')
/*
{}
constructor: ƒ Person()
[[Prototype]]: Object
*/
// 构造函数有一个 constructor 属性,引用其原型对象
// 而这个原型对象也有一个 constructor 属性,引用这个构造函数
// 换句话说:两者循环引用
console.log(Person.prototype.constructor === Person)
console.log('==============================')
// 正常的原型链都会终止于 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__)
console.log('==============================')
// 构造函数,原型对象和实例
// 是 3 个完全不同的对象
let person1 = new Person(),
person2 = new Person();
console.log(person1 !== Person);
console.log(person1 !== Person.prototype);
console.log(Person.prototype !== Person);
console.log('==============================')
// 实例通过 __proto__ 链接到原型对象
// 它实际上指向隐藏隐藏特性[ [Prototype] ]
// 构造函数通过 prototype 属性链接到原型对象
// 实例与构造函数没有直接联系,与原型对象有直接联系
console.log(person1.__proto__ === Person.prototype);
console.log(person1.__proto__.constructor === Person);
console.log('==============================')
// 同一个构造函数创建的两个实例
// 共享同一个原型对象
console.log(person1.__proto__ === person2.__proto__); // true
console.log('==============================')
// instanceof 检查实例的原型链中是否包含指定函数的原型
console.log(person1 instanceof Person);
console.log(person1 instanceof Object);
console.log(Person.prototype instanceof Object);
console.log('==============================')
Person 的原型对象和 Person 现有两个实例之间的关系
- Person.prototype 指向原型对象
- Person.prototype.constructor 指回 Person 构造函数
- 原型对象包含 construcotor 属性和其他后来添加的属性
- Person 的两个实例 person1 和 person2 都只有一个内部属性指回 Person.prototype
- 两者都与构造函数没有直接联系(虽然这两个实例都没有属性和方法,但 person1.sayName 可以正常调用,这是由于对象属性查找机制的原因)
Obj.isPrototypeOf()
isPrototypeOf() 回在传入参数的 [ [Prototype] ] 指向调用它的对象时返回 true
function Person(){}
person1 = new Person();
person2 = new Person();
person3 = new Array();
console.log(Person.prototype.isPrototypeOf(person1))
console.log(Person.prototype.isPrototypeOf(person2))
console.log(Person.prototype.isPrototypeOf(person3))
Obj.getPrototypeOf()
Obj.getPrototypeOf() 返回参数的内部特性 [ [Prototype] ] 的值
Obj.getPrototypeOf() 可以非常方便地取得一个对象的原型
function Person(name){
}
Person.prototype.name = 'Nicholas';
person1 = new Person();
person2 = new Person();
console.log(Object.getPrototypeOf(person1) === Person.prototype);
console.log(Object.getPrototypeOf(person1).name);
Object.setPrototypeOf()
setPrototypeOf()
- 可以向实例私有属性 [ [ Prototype ] ] 写入一个新值(可以重写一个对象的原型继承关系)
let biped = {
numLegs: 2
}
let person = {
name: "Matt"
}
Object.setPrototypeOf(person, biped);
console.log(person.name);
console.log(person.numLegs);
// 因为设置的是 私有特性[ [Prototype] ] 写入一个新值
console.log(Object.getPrototypeOf(person) === biped) // 所以相等
Object.setPrototypeOf() 可能会严重影响代码性能
在所有浏览器 和 Js 引擎中,修改继承关系的影响都是微妙甚远的
这种影响并不是指向 Object.setPrototypeOf() 语句这么简单,而是会涉及所有访问了那些修改过 [ [Prototype] ] 的对象的性能
Object.create()
避免使用 Object.setPrototypeOf() 可能造成的性能下降
可以通过 Object.create() 来创建一个新对象,同时为其指定原型
let biped = {
numLegs: 2
}
let person = Object.create(biped);
person.name = "Matt";
console.log(person.name);
console.log(person.numLegs);
// 因为设置的是 私有特性[ [Prototype] ] 写入一个新值
console.log(Object.getPrototypeOf(person) === biped) // 所以相等
原型层级
在通过对象访问属性时:
- 按照这个属性名开始搜索
- 开始于对象实例本身(在这个实例上发现了给定的名称,则返回对应的值。如果没有找到 ↓)
- 沿着指针进入原型对象(prototype),在原型对象找到属性后返回对应的值)
- 如果没有找到则返回 undefined
constructor 属性只存在于原型对象,通过实例对象也是可以访问到的
虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值
如果在一个实例上添加一个与原型对象中同名的属性(这个属性会遮住原型对象上的属性)
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"; // person1.name 遮蔽了原型对象上的同名属性(虽然不会修改它,但会屏蔽对它的访问)
console.log('person1.name: ', person1.name); // "Greg" 来自实例
console.log('Object.getPrototypeOf(person1).name: ', Object.getPrototypeOf(person1).name) // "Nicholas" 来自实例
console.log('person2.name: ', person2.name); // "Nicholas" 来自原型
delete person1.name // 删除的是 实例对象上的属性 name
console.log('person1.name: ', person1.name)
hasOwnProperty() 属性存在于实例上返回 true
hasOwnProperty() 方法用于确定某个属性是在实例上还是在原型对象上
这个方法继承自 Object 的,会在属性存在于调用它的对象实例上时返回 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"): ', person1.hasOwnProperty("name"));
console.log('====================')
person1.name = "Greg";
console.log('person1.name: ', person1.name); // "Greg" 来自实例
console.log('person1.hasOwnProperty("name"): ', person1.hasOwnProperty("name"));
console.log('====================')
console.log('person2.name: ', person2.name); // "Nicholas" 来自原型
console.log('person2.hasOwnProperty("name"): ', person2.hasOwnProperty("name"));
console.log('====================')
delete person1.name;
console.log('person1.name: ', person1.name); // "Nicholas" 来自原型
console.log('person1.hasOwnProperty("name"): ', person1.hasOwnProperty("name"))
原型 in 操作符
有两种方式使用 in 操作符
- 单独使用
- in 操作符会在可以通过对象访问指定属性时返回 true (无论该属性是在 实例上 还是 原型上)
- for-in 循环中使用
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'): ", person1.hasOwnProperty('name'));
console.log('"name" in person1', "name" in person1)
console.log('=========================')
person2.name = "person2"
console.log("person2.hasOwnProperty('name'): ", person2.hasOwnProperty('name'));
console.log('"name" in person1', "name" in person2)
console.log('=========================')
只要 in 操作符返回 true 且 hasOwnProperty() 返回 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 person = new Person();
console.log(!person.hasOwnProperty("name") && ("name" in person))
person.name = "Greg";
console.log(!person.hasOwnProperty("name") && ("name" in person))
Object.keys()
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('Object.keys(Person.prototype): ', keys) // [ 'name', 'age', 'job', 'sayName' ]
console.log('==============================')
let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
let p1keys = Object.keys(p1)
console.log('Object.keys(p1): ', p1keys) // [ 'name', 'age' ]
console.log('==============================')
Object.getOwnPropertyNames()
Object.getOwnPropertyNames() 列出所有实例属性(无论是否可以枚举)
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.getOwnPropertyNames(Person.prototype);
console.log(keys);
// 返回的结果中包含一个不可枚举的属性 constructor
// Object.keys() 和 Object.getOwnPropertyNames() 在适当的时候都可以用来代替 for-in 循环
Object.getOwnProperty-Symbols()
Object.getOwnProperty-Symbols() 这个方法与 Object.getOwnPropertyNames() 类似,只是针对符号而已
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.getOwnProperty-Symbols() 以及 Object.assign() 在属性枚举顺序方面有很大区别
for-in 循环和 Object.keys() 的枚举顺序是不确定的 取决于 JS 引擎,可能因浏览器而异
Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), Object.assian()
- 枚举顺序是确定性的
- 先以升序枚举数值键,然后插入顺序枚举字符串和符号键
- 在对象字面量中定义的键它们以逗号分隔的顺序插入
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.thrid = 'third';
o[2] = 2;
console.log(Object.getOwnPropertyNames(o)); // [ '0', '1', '2', '3', 'first', 'second', 'thrid' ]
console.log(Object.getOwnPropertySymbols(o)); // [ Symbol(k1), Symbol(k2) ]
对象迭代 Object.values() / Object.entries()
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 sym = Symbol();
const o1 = {
[sym]: 'foo',
}
console.log(Object.values(o1)); // []
console.log(Object.entries(o1)); // []
其他原型语法
直接通过一个包含所有属性和方法的对象字面量来重写原型
function Person() { }
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName(){
console.log(this.name)
}
}
// Person.prototype 被设置为等于一个通过对象字面量创建的新对象
// 最终结果是一样的,只有一个问题
// - Person.prototype 的 constructor 属性就不指向 Person 了
// - 在创建函数时,也会创建它的 prototype 对象,同时会自动给这个原型的 constructor 属性赋值
// - 上面的写法完全重写了默认的 prototype 对象,因此其 constructor 属性也指向了完全不同的新对象(Object)
// - 不再指向原来的构造函数
// - 虽然 instanceof 操作符还能可靠的返回值,但是我们不能在依靠 constructor 属性来识别类型了
let friend = new Person();
console.log('friend instanceof Object: ', friend instanceof Object);
console.log('friend instanceof Person: ', friend instanceof Person);
如果 constructor 的值很重要,则可以像下面这样在重写原型对象时专门设置一下它的值
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName(){
console.log(this.name);
}
}
// 代码中特意包含了 constructor 属性,并将它设置为 Person
// 保证了这个属性仍然包含恰当的值
// 注意
// 以这种方式回复 constructor 属性会创建一个 [ [ Enumerable ] ] 为 true 的属性
// 原生 constructor 属性默认是不可枚举的
// 如果使用的是兼容 ESMAScript 的 JavaScript 引擎,可能会改为使用 Object.defineProperty() 方法来定义 constructor 属性
// - 那可能会改为使用 Object.defineProperty() 方法来定义 constructor 属性
function Person1(){}
Person1.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName(){
console.log(this.name);
}
}
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
原型的动态性
从原型上搜搜值的过程是动态性的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
}
let friend = new Person();
Person.prototype.sayHi = function () {
console.log('hi');
}
friend.sayHi() // hi
虽然随时能给原型添加属性和方法,并能够立即反应在所有对象实例上,但这跟重写整个原型是两回事
实例的 [ [Prototype] ] 指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同的对象也不会变
重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型
实例只有指向原型的指针,并没有指向构造函数的指针
function Person() {
}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName(){
console.log(this.name);
}
}
friend.sayName() // 错误
// Person 新实例时在重写原型对象之前创建的
// 调用 friend.sayName() 的时候,会导致错误(friend)指向的原型还是最初的原型
原生对象原型
所有原生引用类型的构造函数(Object, Array, String) 都在原型上定义了实例方法
数组实例的 sort() 方法就是 Array.prototype 上定义的,
字符串包装对象 substring() 方法也是在 String.prototype 上定义的
console.log(typeof Array.prototype.sort);
console.log(typeof String.prototype.substring);
// 通过原生对象的原型可以取得所有默认方法的引用
// 可以给原生类型实例定义新方法
// 可以像修改自定义对象一样修改原生对象原型,随时可以添加方法/属性
// 当前环境下所有字符串都可以使用该方法
String.prototype.startsWith = function(text){
return this.indexOf(text) === 0;
}
let msg = "Hello World!";
console.log(msg.startsWith("Hello"))console.log(typeof Array.prototype.sort);
console.log(typeof String.prototype.substring);
// 通过原生对象的原型可以取得所有默认方法的引用
// 可以给原生类型实例定义新方法
// 可以像修改自定义对象一样修改原生对象原型,随时可以添加方法/属性
// 当前环境下所有字符串都可以使用该方法
String.prototype.startsWith = function(text){
return this.indexOf(text) === 0;
}
let msg = "Hello World!";
console.log(msg.startsWith("Hello"))
不推荐在产品环境中修改原生对象原型(很可能造成误会,且可能引发命名冲突)
推荐创建一个自定义的类,继承原生类型
原型的问题
弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值
最主要问题源自它的共享特性
- 原型上所有属性是在实例间共享的(对函数来说比较合适)
- 包含引用值的属性
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends: ["Shelby", "Court"],
sayName: function(){
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
// 如果这是有意在多个实例间共享数组,那没什么问题
// 一般来说,不同的实例应该又属于自己的属性副本(这就是实际开发中不单独使用原型模式的原因)
继承
实现继承
- 继承实际方法(主要是通过原型链实现继承的)
原型链
通过原型继承多个引用类型的属性和方法
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());
默认原型
默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的
任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向 Object.prototype
这也是为什么自定义类型能够继承包括 toString(), valueOf() 在内的所有默认方法的原因
原型与继承关系
原型与实例的关系可以通过两种方式来确定
- instanceof 操作符 (如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true)
- isPrototypeOf() 方法(原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,返回 true)
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SubType() {
this.subproperty = false;
}
// 继承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty
}
// instance 是 Object, SuperType, SubType 的实例
// 因为 instance 的原型链中包含这些构造函数的原型
instance = new SubType()
console.log('instance instanceof Object: ', instance instanceof Object)
console.log('instance instanceof SubType: ', instance instanceof SubType)
console.log('instance instanceof SuperType: ', instance instanceof SuperType)
console.log('============================================================')
console.log('Object.prototype.isPrototypeOf(instance): ', Object.prototype.isPrototypeOf(instance))
console.log('SubType.prototype.isPrototypeOf(instance): ', SubType.prototype.isPrototypeOf(instance))
console.log('SuperType.prototype.isPrototypeOf(instance): ', SuperType.prototype.isPrototypeOf(instance))
关于方法
子类有时候需要覆盖父类的方法,或者增加父类没有的方法
- 这些方法必须在原型赋值之后再添加到原型上
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function getSuperValue() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
// 继承 SuperType
SubType.prototype = new SuperType();
// 新方法
SubType.prototype.getSubValue = function getSubValue() {
return this.subproperty
}
console.log(SubType.prototype.getSuperValue.toString())
// 覆盖已有的方法
SubType.prototype.getSuperValue = function getSuperValue() {
return false;
}
let instance = new SubType();
console.log(instance.__proto__.getSubValue.toString())
console.log(instance.__proto__.getSuperValue.toString())
console.log(instance.getSuperValue())
以字面量方式创建原型方法回破坏之前的原型链,因为这相当于重写了原型链
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function getSuperValue() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
// 继承 SuperType
SubType.prototype = new SuperType();
// 通过对象字面量添加新方法,这会导致上一行无效
SubType.prototype = {
getSubValue() {
return this.subproperty;
},
someOtherMethod() {
return false;
}
}
let instance = new SubType();
console.log(instance.getSuperValue()); // 报错
// 子类原型在被赋值为 SuperType 的实例后,又被一个对象字面量覆盖了
// 覆盖后的原型是一个 Object 的实例,而不再是 SuperType 的实例(之前的原型链就断了)
// SubType 和 SuperType 之间也没有关系了
原型链的问题
1. 原型中包含的引用值会在所有实例间共享(属性通常会在构造函数中定义,而不在原型上定义)
- 在使用原型实现继承时,原型实际上变成了另一个类型的实例(原先的实例属性摇身一变成为了原型属性)
2 子类型在实例化时不能给父类型的构造函数传参
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);
let instance2 = new SubType();
console.log(instance2.colors);
盗用构造函数
在子类构造函数中调用父类构造函数。
毕竟函数就是在特定上下文中执行代码的简单对象,
所以可以使用 apply() 和 call() 方法以新创建的对象为上下文执行构造函数。
function SuperType() {
this.colors = ["red", "blue", "green"]
}
function SubType() {
// 继承 SuperType
SuperType.call(this);
}
// 每个实例都有自己的 colors 属性
let instance1 = new SuperType();
instance1.colors.push("black");
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]
let instance2 = new SuperType();
console.log(instance2.colors); // [ 'red', 'blue', 'green' ]
传递参数
相比使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参
function SuperType(name) {
this.name = name;
}
function SubType(name, age){
// 继承 SuperType 并传参
SuperType.call(this, name);
// 实例属性
this.age = age;
}
let instance1 = new SubType("instance1", 18);
let instance2 = new SubType("instance2", 19);
console.log(instance1.name, instance1.age)
console.log(instance2.name, instance2.age)
盗用构造函数的问题
必须在构造函数中定义方法(因此函数不能重用)
子类不能访问父类原型上定义的方法(所有类型只能使用构造函数模式)
盗用构造函数基本上也不能单独使用
组合继承
综合了原型链和盗用构造函数,将两者的有点集中了起来
使用原型链继承原型上的属性和方法
通过盗用构造函数继承实例属性
既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性
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("instance1", 1);
instance1.colors.push('black');
console.log(instance1.colors);
instance1.sayName();
instance1.sayAge();
console.log('==============================')
let instance2 = new SubType("instance2", 2);
console.log(instance2.colors);
instance2.sayName();
instance2.sayAge();
console.log('==============================')
组合继承弥补了原型链和盗用构造函数的不足,是 JS 中使用最多的继承模式,而且组合继承也保留了 instanceof 操作符 和 isPrototypeOf() 方法识别合成对象的能力
原型式继承
即使不自定义类型也可以通过原型实现对象之间的信息共享
function object(o) {
function F(){}
F.prototype = o;
return new F;
}
// object 会创建一个临时函数,将传入的对象赋值给这个构造函数的原型,返回这个零食类的一个实例
// 本质上, object() 是对传入对象执行了一次浅复制
let person = {
name: "Nicholas",
friends: ['1', '2', '3']
}
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);
// person 对象定义了另一个对象也应该共享信息,把它传给 object() 之后会返回一个新对象。
// 这个新对象原型是 person(意味着它的原型上既有原始值属性又有引用值属性)
// 意味着 person.friends 不仅是 person 的属性,也会跟着 anotherPerson 和 yetAnotherPerson 共享
// 这里实际上是克隆了两个 person
Object.create()
Object.create() 与上面 object() 方法效果相同
let person = {
name: "Nicholas",
friends: ['1', '2', '3']
}
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);
Object.create() 的第二个参数与 Object.defineProperties() 的第二参数一样
- 每个新增属性都通过各自的描述符来描述
- 以这种方式添加的属性会遮蔽原型对象上的同名属性
let person = {
name: "Nicholas",
friends: ['1', '2', '3']
}
let anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
})
console.log(anotherPerson.name);
寄生式继承
寄生式继承背后的思路类似于寄生构造函数和工厂模式
- 创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
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: ['1', '2', '3']
}
let anotherPerson = createAnother(person);
anotherPerson.sayHi();
通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似
寄生式组合继承
组合继承其实也存在效率问题。
最主要的效率问题就是父类构造函数始终会被调用两次:
- 一次在是创建子类原型时调用
- 另一次是在子类构造函数中调用
子类原型最终是要包含超累对象的所有实例属性,
子类构造函数只要在执行时重写自己的原型就行了。
function SuperType(name) {
this.name = name;
this.colors = ['1', '2', '3'];
}
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;
}
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype);
prototype.constructor = superType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ['1', '2', '3'];
}
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() 方法正常有效
// 寄生式组合继承可以算是引用类型继承的最佳模式
类
类(class) 是ECMAScript 中新的语法基础性语法糖结构(实际上它背后使用的仍然是原型和构造函数的概念)
类定义
// 类声明
class Person {}
// 类表达式
const Animal = class {};
与函数表达式类似,类表达式在它们被求值之前也不能引用
与函数定义不同的时,函数声明可以提升,但类定义不能
console.log(FunctionExpression); // undefined
var FunctionExpression = function () {};
console.log(FunctionExpression); // function (){}
console.log('==============================')
console.log(FunctionDeclaration); // FunctionDeclaration (){}
function FunctionDeclaration(){}
console.log(FunctionDeclaration); // FunctionDeclaration (){}
console.log('==============================')
console.log(ClassExpression); // undefined
var ClassExpression = class {};
console.log(ClassExpression); // ClassExpression (){}
console.log('==============================')
try {
console.log(ClassDeclaration);
}catch (e){
console.log(e)
}
class ClassDeclaration {}
console.log(ClassDeclaration)
console.log('==============================')
函数声明受函数作用域限制,类受块作用域限制
{
function FunctionDeclaration() {}
class ClassDeclaration{}
}
console.log(FunctionDeclaration)
console.log(ClassDeclaration)
类的构成
类可以包含
- 构造函数方法,
- 实例方法
- 获取函数
- 设置函数
- 静态类方法
- 但这些都不是必须的
空的类定义照样有效
默认情况下,类定义中的代码都在严格模式下执行
建议类名的首字母要大写,以区别于通过它创建的实例(比如,通过 class Foo {} 创建实例 foo)
// 空类定义, 有效
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();
console.log(Person.name)
console.log(PersonName.name)
类构造函数 constructor
constructor 关键字用于在类定义块内部创建类的构造函数
方法名 constructor 会告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数
构造函数的定义不是必须的,不定义构造函数相当于将构造函数定义为空函数
实例化
使用 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();
let v = new Vegetable();
console.log(v.color)
类实例化时传入的参数会用作构造函数的参数。
如果不需要参数,则类名后面的括号也是可选的
class Person{
constructor(name) {
console.log('Person: arguments.length: ', arguments.length)
this.name = name || null
}
}
let p1 = new Person;
console.log(p1.name);
let p2 = new Person();
console.log(p2.name);
let p3 = new Person('Jake');
console.log(p3.name);
默认情况下,类构造函数在执行之后返回 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)
console.log(p1 instanceof Person)
console.log(p2)
console.log(p2 instanceof Person)
类构造函数与构造函数的区别是
- 调用类构造函数必须使用 new 操作符
- 普通构造函数如果不适用 new 调用,会以全局 this (通常是 widnow) 作为内部对象
- 调用类构造函数时如果忘了使用 new 则会抛出错误
function Person() {}
class Animal {}
// 把 window 作为 this 来构建实例
let p = Person();
let a = Animal();
类构造函数没有什么特别之处,实例化之后,它会成为普通的实例方法(但作为类构造函数,仍然要使用 new 调用)因此,实例化之后可以在实例上引用它
class Person {}
// 使用类创建一个新实例
let p1 = new Person();
// p1.constructor(); // 报错
// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor()
把类当成特殊函数
ECMAScript 类就是一种特殊函数
声明一个类之后,使用 typeof操作符检测类标识符,表明它是一个函数
class Person {}
console.log(Person); // class Person
console.log(typeof Person); // function
类标识符有 prototype 属性,而这个原型也有一个 constructor 属性指向类自身
class Person {
}
console.log(Person.prototype);
console.log(Person === Person.prototype.constructor)
可以使用 instanceof 操作符检查一个对象与类构造函数,以确定这个对象是不是类的实例
类本身具有与普通构造函数一样的行为。
类的上下文中,类本身在使用 new 调用时就会被当成构造函数
类中定义的 constructor 方法不会被当成构造函数,在对它使用 instanceof 操作符时会返回 false
如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么 instanceof 操作符的返回值会反转
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
console.log('=========================')
let p2 = new Person.constructor();
console.log(p2.constructor === Person);
console.log(p2 instanceof Person);
console.log(p2 instanceof Person.constructor);
类是 JS 的一等共鸣,因此可以像其他对象或函数引用一样把类作为参数传递
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
console.log('==============================')
// 与立即调用函数表达式相似,类也可以立即实例化
// 因为类是一个表达式, 所以类名是可选的
let p = new class Foo{
constructor(x) {
console.log(x)
}
}('bar'); // bar
console.log(p)
实例,原型和类成员
类语法可以非常方便地定义存在于实例上的成员,
应该存在于原型上的成员,以及应该存在于类本身的成员。
实例成员
通过 new 调用类标识符时,都会执行类构造函数
在这个函数内部,可以为新创建的实例(this)添加 “自有” 属性。
至于添加什么样的属性,则没有限制、
在构造函数执行完毕后,仍然可以给实例继续添加新成员
每个实例都对应一个唯一的成员对象,意味着所有成员都不会在原型上共享
class Person {
constructor() {
// 这个例子先使用对象包装类型定义的一个字符串
// 为的是在下面测试两个对象的相等性
this.name = new String('Jack');
this.sayName = () => console.log(this.name); // 箭头函数
this.nickname = ["Jake", "J-Dog"];
}
}
let p1 = new Person(),
p2 = new Person();
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nickname === p2.nickname); // false
console.log('==============================')
p1.name = p1.nickname[0]
p2.name = p2.nickname[0]
p1.sayName();
p2.sayName();
原型方法与访问器
为了在实例间共享 方法,类定义语法把类块中定义的方法作为原型方法
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()
console.log('==============================')
p[symbolKey]()
console.log('==============================')
p.computedKey()
console.log('==============================')
类定义也支持获取和设置访问器。语法与行为跟普通对象一样
class Person {
set name(newName){
this.name_ = newName;
}
get name(){
return this.name_;
}
}
let p = new Person();
p.name = "Jake";
console.log(p.name);
静态类方法
可以在类定义静态方法。
这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。
与原型成员类似,静态成员每个类上只能有一个
// 静态成员在了定义使用 static 关键字作为前缀
// 在静态成员中,this 引用类自身。
// 其他所有约定跟原型成员一样
class Person {
constructor() {
// 添加 this 的所有内容都会存在于不同实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型对象上
locate () {
console.log('class', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate();
Person.prototype.locate();
Person.locate()
静态类方法非常适合作为实例工厂
class Person{
constructor(age){
this.age_ = age;
}
sayAge(){
console.log(this._age);
}
static create(){
// 使用随机年龄创建并返回一个 Person 实例
return new Person(Math.floor(Math.random() * 1000));
}
}
console.log(Person.create());
非函数原型和类成员
虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
// 在类上定义数据成员
Person.greeting = 'My name is';
// 在原型上定义数据成员
Person.prototype.name = "Jake";
let p = new Person();
p.sayName();
类定义中之所以没有显式支持添加数据成员,是因为在共享目标(原型和类)上添加可变(可修改)数据成员是一种反模式
一般来说,对象实例应该独自拥有通过 this 引用的数据
迭代器与生成方法
类定义语法支持在原型和类本身上定义生成器方法
class Person {
// 在原型上定义生成器方法
* createNicknameIterator() {
yield 'Jack';
yield 'Jace';
yield 'J-Dog';
}
// 在类上定义生成器方法
static *createJobIterator(){
yield 'Butcher';
yield 'Baker';
yield 'candlestick maker';
}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value)
console.log(jobIter.next().value)
console.log(jobIter.next().value)
console.log('==============================')
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value)
console.log(nicknameIter.next().value)
console.log(nicknameIter.next().value)
因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象
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(idx, nickname)
}
也可以只返回迭代器实例
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(idx, nickname)
}
继承
ES6 新增特性中最出色的一个就是原生支持了类继承机制
虽然类继承使用的是新语法,但背后依旧使用的是原型链
继承基础
ES6 类支持单继承
使用 extends 关键字,就可以继承任何拥有 [ [Construct] ]和原型的对象
意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)
继承类
class Vehicle { }
// 继承类
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Object);
console.log(b instanceof Bus);
console.log(b instanceof Vehicle);
继承普通构造函数
function Person() {}
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Object);
console.log(e instanceof Engineer);
console.log(e instanceof Person);
派生类都会通过原型链访问到类和原型上定义的方法
this 的值会反映调用相应方法的实例或者类
class Vehicle{
identifyPrototype(id){
console.log('Vehicle prototype:', id, this);
}
static identifyClass(id){
console.log('Vehicle static:', id, this);
}
}
class Bus extends Vehicle {}
let v = new Vehicle();
let b = new Bus();
v.identifyPrototype('vehicle');
b.identifyPrototype('bus');
console.log('===========================')
Bus.identifyClass('bus');
Vehicle.identifyClass('vehicle');
构造函数, HomeObject 和 super()
派生类的方法可以通过 super 关键字引用它们的原型
这个关键字只能在派生类中使用,而且仅限于类构造函数,实例方法和静态方法内部
在类构造函数中使用 super 可以调用父类构造函数
class Vehicle{
constructor() {
this.hasEngine = true;
}
}
class Bus extends Vehicle {
constructor() {
// 不要在调用 super() 之前引用 this,否则会抛出 ReferenceError
super(); // 相当于 super.constructor()
console.log(this instanceof Vehicle);
console.log(this);
}
}
new Bus();
在静态方法中可以通过 super 调用继承的类上定义的静态方法
class Vehicle {
static identify(){
console.log('class Vehicle static Func');
}
}
class Bus extends Vehicle{
static identify(){
super.identify();
}
}
Bus.identify();
super 始终会定义为 [ [HomeObject] ] 的原型
使用 super 时要注意的几个问题
super 只能在派生类构造函数和静态方法中使用
// super 只能在派生类构造函数和静态方法中使用
class Vehicle {
constructor() {
super(); // SyntaxError: 'super' keyword unexpected
}
}
不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法
class Vehicle{}
class Bus extends Vehicle {
constructor() {
console.log(super); // SyntaxError: 'super' keyword unexpected here
}
}
调用 super() 会调用父类构造函数,并将返回的实例赋值给 this
class Vehicle {}
class Bus extends Vehicle{
constructor() {
super();
console.log(this instanceof Vehicle);
}
}
new Bus();
super() 的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {
constructor(licensePlate) {
super(licensePlate);
}
}
console.log(new Bus('123456'))
如果没有定义类构造函数,在实例化派生类时会调用 super()
而且会传入所有给派生类的参数
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {}
console.log(new Bus('123456789'))
在类构造函数中,不能再调用 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())
console.log(new Bus())
console.log(new Van())
抽象基类
抽象基类:它可供其他类继承,但本身不会被实例化
ECMAScript 没有专门支持这种类的语法,但通过 new.targer 也很容易实现
new.target 保存通过 new 关键字调用的类或函数
通过在类实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化
class Vehicle{
constructor() {
console.log('Vehicle new.target: ', 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
通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。
因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法
// 抽象基类
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(); // throw new 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);
console.log(a instanceof SuperArray);
console.log(a);
a.shuffle();
console.log(a);
有些内置方法会返回新实例。默认情况下,返回实例地类型与原始实例地类型是一致的
class SuperArray extends Array {
}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x % 2));
console.log(a1);
console.log(a2);
console.log(a1 instanceof Array)
console.log(a2 instanceof SuperArray)
如果想覆盖这个默认行为,则可以覆盖 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);
console.log(a2);
console.log(a1 instanceof SuperArray)
console.log(a2 instanceof SuperArray)
类混入(多继承)
把不同类集中到一个类是一种常见的 JavaScript 模式
ES6 没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为
Object.assign() 方法是为了混入对象行为而设计的。
只有在需要混入类的行为时才有必要自己实现混入表达式
如果只是需要混入多个对象的属性,那么可以使用 Object.assign() 就可以了
class Vehicle {}
function getParentClass() {
console.log('evaluated expression');
return Vehicle;
}
// extends 关键字后面是一个 JavaScript 表达式
// 任何可以解析为一个类或一个构造函数的表达式都是有效的
// 这个表达式会在求值类定义时被求值
class Bus extends getParentClass() {}
// 可求值的表达式
// 混入模式可以通过在一个表达式中连缀多个混入元素来实现,
// 这个表达式最终会解析为一个可以被继承的类
// 如果 Person 类需要组合 A, B, C, D 则需要某种机制实现 B 继承 A, C 继承 B
// 而 Person 再继承 C,从而把 A, B, C 组合到这个超类中
// 定义一组 "可嵌套" 的函数,每个函数分别接收一个超累作为参数,
// 而将混入类定义为这个参数的子类,并返回这个类
// 这些组合函数可以连缀调用,最终组合成超累表达式
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();
b.bar();
b.baz();
// 混入模式可以通过在一个表达式中连缀多个混入元素来实现,
// 这个表达式最终会解析为一个可以被继承的类
// 如果 Person 类需要组合 A, B, C, D 则需要某种机制实现 B 继承 A, C 继承 B
// 而 Person 再继承 C,从而把 A, B, C 组合到这个超类中
// 通过写一个辅助函数,可以把嵌套调用展开
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();
b.bar();
b.baz();
```