第8章 对象 类 面向对象编程
第8章 对象、类、面向对象编程
属性类型
ECMA-262使用内部特性来描述属性的特征。共有两种类型的属性:数据属性和存取器属性。
数据属性
数据属性的四个特性:
[[Configurable]] 默认为true。若为true,则可使用delete删除属性,修改属性的特性,修改属性为存取器属
性。
[[Enumerable]] 默认为true。若为true,可被for...in循环返回。
[[Writable]] 默认为true。若为true,属性的值可被改变。
[[Value]] 默认为undefined。 包含属性的值。
修改属性特性使用Object.defineProperty()方法,该方法接受三个参数:要添加或修改的属性的对象,属性名,和一个描述符对象。描述符对象的属性必须是特性名configurable,enumerable,writable,和value。
let person = {};
Object.defineProperty(person,'name',{
writable:false,
value:'Ciri',
//除value外其他三个特性默认将为false
})
console.log(person.name);//Ciri
person.name = 'Geralt';//严格模式将报错
console.log(person.name);//Ciri
let descriptor =
Object.getOwnPropertyDescriptor(person,'name');
console.log(descriptor);
//{value: "Ciri", writable: false, enumerable: false, configurable: false}
修改Configurable为false后不可修改可配置性及其他特性,delete无法删除属性。如下:
let person = {};
Object.defineProperty(person,'name',{
configurable:false,
writable:false,
value:'Ciri'
})
console.log(person.name);//Ciri
person.name = 'Geralt';
console.log(person.name);//Ciri
delete person.name;
console.log(person.name);//Ciri
Object.defineProperty(person,'name',{
//writable:true,//异常
//configurable:true//异常
})
除非指定具体值否则使用该方法时configurable,enumerable,writable的默认值为false。
存取器属性
存取器属性不包含数据值,他们包含getter函数和setter函数的组合。当读取存取器属性时,将调用getter函数,该函数负责返回有效值;当写入存取器属性时,将使用新的值调用一个函数。
存取器属性也有四个特性:
[[Configurable]] 默认为true。若为true,则可使用delete删除属性,修改属性的特性,修改属性为数据属
性。
[[Enumerable]] 默认为true。若为true,可被for...in返回。
[[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
定义多个属性
使用Object.defineProperties()方法,该方法使用描述符对象一次定义多个属性,接受的两个参数为:要修改属性的对象,和对应要增加或修改的属性名。
let book = {};
Object.defineProperties(book, {
title: {
value: '昆特牌指南'
},
author: {
value: 'Geralt'
},
_year: {
value: 2018
},
playGwent: {
get: function() {
return `${this.author}陪你玩`;
},
set: function(newValue) {
if (newValue > this._year) {
this._year = newValue; //_year的configurable为false,写入失败。
}
}
}
})
console.log(book.playGwent); //Geralt陪你玩
book.playGwent = 2020;
console.log(book._year); //2018
let descriptor = Object.getOwnPropertyDescriptor(book, 'playGwent');
console.log(descriptor);
//{enumerable: false, configurable: false, get: ƒ, set: ƒ}
console.log(Object.getOwnPropertyDescriptors(book));
// {
// author: {
// value: "Geralt",
// writable: false,
// enumerable: false,
// configurable: false
// },
// playGwent: {
// enumerable: false,
// configurable: false,
// get: ƒ,
// set: ƒ
// },
// title: {
// value: "昆特牌指南",
// writable: false,
// enumerable: false,
// configurable: false
// },
// _year: {
// value: 2018,
// writable: false,
// enumerable: false,
// configurable: false
// }
// }
读取属性特性
可使用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 descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor02 = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor02.value); // undefined
console.log(descriptor02.enumerable); // false
console.log(typeof descriptor02.get); // "function"
合并对象
ES6新方法Object.assign(),该方法接受一个目标对象,一个或多个源对象,复制源对象可枚举(Object.propertyIsEnumerable返回true)自有(Object.hasOwnProperty返回true)属性到目标对象。属性键为string和symbol都会复制。
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 }
//getter和setter
dest = {
set a(val) {
console.log(`调用目标对象setter时使用参数:${val}`);
}
};
src = {
get a() {
console.log('调用源对象setter');
return 'foo';
}
};
Object.assign(dest, src);
// 调用源对象setter
// 调用目标对象setter时使用参数:foo
// 存取器不会被复制
console.log(dest); // { set a(val) {...} }
如果源对象有相同的属性,则其值为最后复制的值:
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 }
另外,任何从存取器获取的值,如源对象getter获取的值将作为静态值赋给目标对象。所以不能在对象间转移getter和setter:
// 这可通过在目标对象上使用setter观察到
let dest = {
set id(x) {
console.log(x);
}
};
Object.assign(dest, {id: 'first'}, {id: 'second'}, {id: 'third'});
// first
// second
// third
console.log(dest); //{set id: ƒ id(x)} 目标对象id为setter
//对象引用
dest = {};
src = {
a: {}
};
Object.assign(dest, src);
// 对对象属性执行浅复制,仅复制引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true
对象相等
ES6之前 '==='的不足之处:
// 符合预期
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false
// 待纠正
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 待纠正
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
为了解决这些问题,ES6规范添加了Object.is()方法,该方法行为如同'==='但额外处理了上述问题:
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 纠正
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 纠正
console.log(Object.is(NaN, NaN)); // true
当检查超过两个对象时可使用递归:
let a = [6, 6, 6, 6, 6, 6];
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}
console.log(recursivelyCheckEqual(6, ...a));//true
高级对象语法
ES6里引入了极其有用的句法工具用于定义对象或与对象交互。
属性简写
let name = 'Ciri';
let obj = {
//分号可省略
//name: name
name
};
console.log(obj); //{name: "Ciri"}
计算属性键
在引入计算属性键之前,无法给对象字面量动态分配键,除了使用方括号将每个键包起来:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
[nameKey]: 'Geralt',
[ageKey]: 99,
[jobKey]: 'Gwent player'
};
console.log(person); //name: "Geralt", age: 99, job: "Gwent player"}
键周围方括号提示运行时将其内容当成表达式而不是字符串:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`;
}
let person = {
[getUniqueKey(nameKey)]: 'Geralt',
[getUniqueKey(ageKey)]: 99,
[getUniqueKey(jobKey)]: 'Gwent player'
};
console.log(person); //{name_0: "Geralt", age_1: 99, job_2: "Gwent player"}
简洁方法语法
当定义对象的函数属性时:
let person = {
sayName: function(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Ciri'); // My name is Ciri
简写模式:
let person = {
sayName(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Ciri'); // My name is Ciri
get和set也适用:
let person = {
name_: '',
get name() {
return this.name_;
},
set name(name) {
this.name_ = name;
},
sayName() {
console.log(`My name is ${this.name_}`);
}
};
person.name = 'Ciri';
person.sayName(); //My name is Ciri
方法简写与计算属性键值一起很配:
const methodKey = 'sayName';
let person = {
[methodKey](name){
console.log(`My name is ${name}`);
}
};
person.sayName('Ciri');//My name is Ciri
对象解构
ES6引入了对象解构,允许在单个语句中使用嵌套数据执行一个或多个操作。对于对象,能够使用匹配对象结构的语法(即“{}”)从对象属性执行赋值。下面是两个相同功能的代码片段。
未使用解构:
let person = {
name: 'Ciri',
age: 18
};
let personName = person.name;
let personAge = person.age;
console.log(personName); //Ciri
console.log(personAge); //18
使用解构:
let person = {
name: 'Ciri',
age: 18
};
let {
name: personName,
age: persnonAge
} = person;
console.log(personName); //Ciri
console.log(persnonAge); //18
如果要重用属性名作为局部变量名,可以简写为:
let person = {
name: 'Ciri',
age: 18
};
let {
name,
age
} = person;
console.log(name); //Ciri
console.log(age); //18
解构只匹配对象中存在的属性,对象中引用了但未赋值的属性将设置为undefined;未在源对象中的属性可赋予默认值,否则将被覆写。如下面的interest和age属性:
let person = {
name: 'Ciri',
age: 18
};
let {
name,
age = 99,
job,
interest = 'hunt monster'
} = person;
console.log(name); //Ciri
console.log(age); //18
console.log(job); //undefined
console.log(interest); //hunt monster
解构使用内部函数ToObject()(运行时无法直接访问)将源(=右边)强制转换为对象,这意味着解构时基本值类型被当作对象,并且null和undefined不能解构,会抛异常:
var {
length
} = 'abcde'; //字符串'abcde'的length属性值赋给length
console.log(length); //5
console.log(window.length);//5
let {
constructor: c
} = 4;//将"4"的constructor属性赋给c
console.log(c === Number); //true
//let {n} = null;
//Cannot destructure property 'n' of 'null' as it is null.
//let {u} = undefined;
//Cannot destructure property 'u' of 'undefined' as it is undefined.
解构不要求变量声明发生在解构表达式中,若这样做的话赋值表达式应包含在圆括号里:
let personName, personAge;//在这里声明变量
let person = {
name: 'Ciri',
age: 18
};
({name:personName,age:personAge} = person);//此处加'()'
// console.log(name);
// console.log(age);报错
console.log(personName, personAge); // Ciri 18
嵌套解构
解构中引用嵌套属性或分配目标没有任何限制。如下复制对象属性:
let person = {
name: 'Ciri',
age: 18,
job: {
title: 'witcher'
}
};
let personCopy = {};
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person);
console.log(personCopy); //{name: "Ciri", age: 18, job:{title:'witcher'}}
person.job.title = 'Gwent player';
console.log(personCopy); //{name: "Ciri", age: 18, job:{title: "Gwent player"}}
console.log(person.job === personCopy.job); //true
console.log(person === personCopy); //false
let {
job: {
title
}
} = person;
console.log(title); //Gwent player
解构赋值可嵌套,以匹配嵌套属性引用:
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
({
foo: {
//Cannot read property 'bar' of undefined
bar: personCopy.bar
}
} = person);
({
job: {
//Cannot set property 'title' of undefined
title: personCopy.job.title
}
} = person);
部分解构补全(Partial Destructuring Completion)
解构赋值涉及多个属性时是有单独结果的一系列操作,如中途遇异常,解构分配将退出,导致仅部分完成分配,如下:
let person = {
name: 'Ciri',
age: 18
};
let personName, personAge, personJob;
try {
({
name: personName,
job: {
jb: personJob
},
age: personAge
} = person);
} catch (e) {
//TODO handle the exception
}
console.log(personName, personJob, personAge);
//Ciri undefined undefined
形参环境匹配
可在函数形参列表里执行解构分配,且不影响arguments对象。可在函数签名中声明变量,这些变量可立即在函数体内使用:
let person = {
name: 'Ciri',
age: 18
};
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('hey', person, 'ha');
//["hey", {name: "Ciri", age: 18} "ha"]
//Ciri 18
printPerson2('hey', person, 'ha');
//["hey", {name: "Ciri", age: 18} "ha"]
//Ciri 18
对象创建
ES6开始正式支持类和继承。
工厂模式
缺点:无法解决对象识别的问题(是什么类型的对象) 。
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("Ciri", 18, "monst hunter");
let person2 = createPerson("Geralt",100, "Witcher");
函数构造函数模式
缺点:每次创建实例时都创建相同的方法,而不能共享之 。 如下所示:
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("Ciri", 18, "monst hunter");
let person2 = new Person("Geralt",100, "Witcher");
与createPerson()相比,Person()有如下不同:
没有显示创建对象。
属性和方法直接分配到this对象上。
没有return语句。
使用new实例化。
当使用new创建Person实例时:
在内存中创建一个新对象。
将新对象内部的[[Prototype]]指针分配给构造函数的prototype属性。
构造函数的this值分配给新对象(所以this指向新对象)。
执行构造函数中的代码(将属性添加到新对象)。
如果构造函数返回一个非null值,则返回该对象。否则,将返回刚刚创建的新对象。
constructor属性最初用于识别对象类型,但是instanceof操作符被认为是更安全的确定类型的方式。
//也可以使用函数表达式
//let Person = function(name, age, job) {
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
// 不传递任何参数时后面()可以省略
// let person2 = new Person;
let person1 = new Person("Ciri", 18, "monst hunter");
let person2 = new Person("Geralt",100, "witcher");
person1.sayName();//Ciri
person2.sayName();//Geralt
console.log(person1.constructor === Person); // true constructor属性在原型上
console.log(person2.constructor === Person); // true
将构造函数作为函数
构造函数和其他函数之间的唯一区别是调用它们的方式。毕竟,构造函数也是函数。没有使用特殊的语法来定义一个构造函数。使用new运算符调用的任何函数都充当构造函数,而没有使用new运算符调用的任何函数都可以像正常函数调用那样起作用。如调用上例Person:
// 用作构造函数
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"
构造函数的缺点:每个实例都会创建方法(如果有的话),而函数是对象,每当定义函数时实际上是实例化对象。让Function的每个实例做同样的事情没有意义,尤其是this对象可将函数绑定到特定对象:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
原型模式
每个函数都有一个prototype属性,该属性是一个对象。该对象实际上是调用构造函数后要创建的对象的原型。 使用原型的好处是它的所有属性和方法在对象实例之间共享 。
每个原型会自动获得一个constructor属性,该属性指向函数本身 :
function Person() {
Person.prototype.name = 'Ciri';
Person.prototype.age = 18;
Person.prototype.job = 'witcher';
Person.prototype.sayName = function() {
console.log(this.name);
};
}
console.log(Person.prototype.constructor === Person); //true
let person1 = new Person();
person1.sayName();//Ciri
let person2 = new Person();
person2.sayName();//Ciri
console.log(person1.sayName == person2.sayName);//true
//或
let Person = function(){};
Person.prototype.name = 'Ciri';
Person.prototype.age = 18;
Person.prototype.job = 'witcher';
Person.prototype.sayName = function (){console.log(this.name);};
let person1 = new Person();
person1.sayName();//Ciri
let person2 = new Person();
person2.sayName();//Ciri
console.log(person1.sayName == person2.sayName);//true
原型工作原理
每当函数创建时,其prototype属性也根据具体的规则随之创建。默认情况下,所有的原型属性自动获得一个叫constructor的属性,该属性指向函数本身。
每次调用构造函数创建实例时,每个实例都有一个内部指针指向构造函数的原型。ECMA-262中叫[[Prototype]],没有标准途径从脚本中访问该指针。但一些浏览器指出用__proto__
属性访问。
function Person() {}
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: ƒ Object()
// hasOwnProperty: ƒ hasOwnProperty()
// isPrototypeOf: ƒ isPrototypeOf()
// propertyIsEnumerable: ƒ propertyIsEnumerable()
// toLocaleString: ƒ toLocaleString()
// toString: ƒ toString()
// valueOf: ƒ valueOf()
// __defineGetter__: ƒ __defineGetter__()
// __defineSetter__: ƒ __defineSetter__()
// __lookupGetter__: ƒ __lookupGetter__()
// __lookupSetter__: ƒ __lookupSetter__()
// get __proto__: ƒ __proto__()
// set __proto__: ƒ __proto__()
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.constructor === Person); // true
console.log(person1.__proto__ === person2.__proto__); // true
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name);//Ciri
构造函数与实例关系类图如下:
注意:使用Object.setPrototypeOf()操作可能会导致严重的性能问题。
为了避免这种情况,可使用Object.create()方法:
let biped = {
numLegs: 2
};
let person = {
name: 'Ciri'
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Ciri
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
//建议替换为
let biped = {
numLegs: 2
};
let person = Object.create(biped);
person.name = 'Ciri';
console.log(person.name); // Ciri
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
理解原型层级
可以读取原型上的值,但是不能覆写它们。 如果添加的属性与实例原型上的属性重名,则在实例上添加该属性 ,原型上同名属性被遮挡 。
添加的属性设置为null,不能重新获取原型上的同名属性,但使用delete删除,则可再次获取原型上属性:
function Person() {}
Person.prototype.name = "Geralt";
Person.prototype.age = 99;
Person.prototype.job = "witcher";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = "Ciri";
console.log(person1.name); // Ciri 来自实例
console.log(person2.name); //Geralt 来自原型
person1.name = null;
console.log(person1.name); //null
delete person1.name;
console.log(person1.name); //Geralt
原型和in操作符
in可直接使用或在for...in循环中使用。直接使用时当给定名称的属性可通过对象访问时返回true,也就是说该属性可能存在于实例或原型上。
for...in可遍历实例属性和原型属性 。
Object.keys()获取可枚举实例属性名并返回一个数组 :
function Person() {}
Person.prototype.name = "Geralt";
Person.prototype.age = 99;
Person.prototype.job = "witcher";
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"]
//获取自身可枚举和不可枚举属性
let keys2 = Object.getOwnPropertyNames(Person.prototype);
console.log(keys2);
//["constructor", "name", "age", "job", "sayName"]
console.log(Object.getOwnPropertyNames(p1));//["name", "age"]
ES6引入了Object.getOwnPropertySymbols()方法:
let k1 = Symbol('k1'),
k2 = Symbol('k2');
let o = {
[k1]: 'k1',
[k2]: 'k2'
};
console.log(Object.getOwnPropertySymbols(o));
//[Symbol(k1), Symbol(k2)]
属性枚举顺序
Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() 和 Object.assign()有固定的属性枚举顺序;for-in循环和Object.keys()则没有。
Object.assign()复制一个或多个源的可枚举自有属性到目标对象,并返回修改后的目标对象:
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }
对于 固定枚举顺序的方法,number键将首先按升序枚举,然后string和symbol键按插入顺序枚举;对象字面量里的键值按逗号分隔顺序枚举 :
let k1 = Symbol('k1'),
k2 = Symbol('k2');
let o = {
1: 1,
first: 'first',
[k1]: 'sym1',
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)]
对象迭代
ES2017介绍了两个静态方法Object.values()和Object.entries(),这两个方法将对象内容转换为连续的可迭代的格式。都接受一个对象参数,Object.values()返回对象值的数组,Object.entries()返回键值对的数组。symbol键属性将被忽略。
const sb = Symbol('id');
const o = {
foo: 'bar',
baz: 1,
qux: {},
[sb]:666
};
console.log(Object.values(o));//["bar", 1, {...}]
console.log(Object.entries((o)));//[["foo", "bar"], ["baz", 1], ["qux", {}]]
console.log(Object.values(o)[0]);//bar
console.log(Object.entries(o)[1][0]);//baz
可选的原型语法
如下所示:
function Person() {}
Person.prototype = {
//constructor:Person,可手动设置
name: "Geralt",
age: 99,
job: "witcher",
sayName() {
console.log(this.name);
}
};
// Object.defineProperty(Person.prototype, "constructor", {
//
enumerable: false,
//
value: Person
// });不可枚举
console.log(Person.prototype.constructor); //ƒ Object() { [native code] }
let friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true
此例中,Person.prototype属性设置为一个新的对象字面量,其constructor属性不再指向构造函数(而是创建新原型对象的Object()构造函数)。
原型的动态性质
在构造函数上覆写原型意味着新实例将引用新原型,而任何先前存在的对象实例仍引用旧原型。
function Person() {}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
//let friend = new Person();这里实例化正常
friend.sayName();//Uncaught TypeError: friend.sayName is not a function
原生对象原型
原生引用类型也支持原型,如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
继承
关于OO编程最常讨论的概念是继承。许多OO语言支持两种继承:接口继承(仅继承方法签名)和实现继承(继承实际方法)。 ECMAScript中无法进行接口继承,因为函数没有签名。实现继承是ECMAScript支持的唯一继承类型,这主要是通过使用原型链来完成的。
原型链
ECMA-262将原型链作为ECMAScript中继承的基本方式。基本思想是使用原型的概念在两个引用类型之间继承属性和方法。回忆一下构造函数、原型和实例之间的关系:每个构造函数都有一个指向该构造函数的原型对象,而实例则具有一个指向该原型的内部指针。如果原型实际上是另一种类型的实例怎么办?这意味着原型本身将具有指向不同原型的指针,而该原型又有指向另一个构造函数的指针。如果该原型也是另一个类型的实例,则该模式将继续,从而在实例和原型之间形成一条链。这是原型链接背后的基本思想。
原型链涉及如下代码模式:
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
// 覆写存在的方法
SubType.prototype.getSuperValue = function() {
return false;
};
类图如下:
使用方法
通常情况下,子类需要覆写父类方法或添加父类不存在的方法,这样的话方法必须在原型赋值后添加,如下所示:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 继承自SuperType
SubType.prototype = new SuperType();
// 添加新方法
SubType.prototype.getSubValue = function() {
return this.subproperty;
};
// 覆写存在的方法
SubType.prototype.getSuperValue = function() {
return false;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // false
使用对象字面量创建新原型不能与原型链一起使用,因为会覆盖原型链:
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() {
return this.subproperty;
},
someOtherMethod() {
return false;
}
};
let instance = new SubType();
console.log(instance.getSuperValue());
// Uncaught TypeError: instance.getSuperValue is not a function
原型链的缺点
原型的引用值在所有实例间共享
子类实例创建时不能传递参数给父类构造函数
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"
构造函数窃取
constructor stealing(有时也叫对象伪装或经典继承)用来解决 引用属性在实例间共享的问题 。
在子类构造函数创建新对象时调用父类的构造函数进行初始化,这样每个实例都有一份自己的属性拷贝。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
//调用父类构造函数
SuperType.call(this);
}
//实例创建5步曲
//1. 在内存中创建一个新对象
//2. 将新对象内部的[[Prototype]]指针分配给构造函数的prototype属性
//3. 构造函数的this值分配给新对象
//4. 执行构造函数中的代码(将属性添加到新对象)
//5. 如果构造函数返回一个非null值,则返回该对象。否则,将返回刚刚创建的新对象。
let instance1 = new SubType();
//步骤:
//1,创建一个新对象
//2,将新对象内部的[[Prototype]]指针指向SubType的原型SubType.prototype
//3,构造函数SubType的this值分配给新对象
//4,执行父类构造函数的代码,添加属性到新对象
//5返回新对象
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.call(this, "Ciri");
//实例属性
this.age = 18;
}
let instance = new SubType();
console.log(instance.name); // "Ciri";
console.log(instance.age);//18
组合继承
基本思想:使用原型链继承原型上的属性和方法,并使用构造函数窃取来继承实例属性。这样可以通过在原型上定义方法来重用函数,并允许每个实例拥有自己的属性:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
//继承父类俩属性name,colors
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Ciri", 18);
//分析:
//创建一个新对象
//将新对象实例内部的[[Prototype]]指针指向SubType.prototype
//分配构造函数SubType的this值给新对象
//调用父类构造函数分配name、colors属性到新对象,接着添加age属性到新对象
//返回新对象
console.log(Object.keys(instance1));//["name", "colors", "age"] 实例上可枚举属性
console.log(Object.getOwnPropertyNames(instance1));//["name", "colors", "age"]
for(const p in instance1){
console.log(p);//name colors age sayAge sayName
}
for(const p in instance1){
if(instance1.hasOwnProperty(p)){
console.log(p);//name colors age
}
}
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Ciri";
instance1.sayAge(); // 18
let instance2 = new SubType("Geralt", 99);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Geralt";
instance2.sayAge(); // 99
原型继承
2006 年,道格拉斯·克罗克福德(Douglas Crockford)写了一篇文章《JavaScript中的原型继承》,其中他介绍了一种不涉及使用严格定义的构造函数的继承方法。他的前提是原型允许基于现有对象创建新对象,而无需定义自定义类型。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
object()函数创建一个临时构造函数,将传入参数作为其原型,然后返回临时类型的实例。
ES5引入的Object.create()方法正是使用了这种概念。该方法接受两个参数:新对象使用的原型对象,另一个参数同Object.defineProperties()的第二个参数,为新对象要添加的属性的描述符对象。
let person = {
name: 'Geralt',
friends: ['Triss', 'Yennefer']
}
let p1 = Object.create(person);
p1.friends.push('Ciri');
let p2 = Object.create(person, {
age: {
value: 18
}
});
console.log(p2.name); //Geralt
p2.friends.push('Vesemir');
console.log(p2.friends); // ["Triss", "Yennefer", "Ciri", "Vesemir"]
console.log(p1.friends); // ["Triss", "Yennefer", "Ciri", "Vesemir"]
console.log(p2); //{age: 18}
寄生继承
与原型继承密切相关的是寄生继承,这是由Crockford推广的另一种模式。寄生继承的思想类似于寄生构造函数和工厂模式:创建一个函数用来继承,以某种方式扩充对象,然后返回对象:
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"
寄生组合继承
组合继承并非完美。模式中最无效的部分是,总是会调用父类型构造函数两次:一次是创建子类的原型时,一次是子类的构造函数中。本质上,子类原型以父类对象实例的所有属性结束,只是在执行子类构造函数时才将其覆写。实例如下:
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); // second call to SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); // first call to 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); // create object
prototype.constructor = subType; // augment object
subType.prototype = prototype; // assign object
}
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()的行为都与正常情况相同。寄生组合继承被认为是引用类型的最佳继承范式。
类
类定义基础
定义类的两种方式——类声明和类表达式:
//类声明
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
类组成
一个类可由类的构造函数方法、实例方法、getter、setter、静态类方法组成。
当类表达式赋值给变量时,可以从name属性获取类表达式名称。但 类名无法在类之外被访问 。
let Person = class GoodPerson{
getClassName(){
console.log(Person.name,GoodPerson.name);
}
}
let p = new Person();
p.getClassName();//GoodPerson GoodPerson
console.log(Person);//class GoodPerson{...}
console.log(Person.name);//GoodPerson
console.log(GoodPerson);//ReferenceError: GoodPerson is not defined
console.log(GoodPerson.name);//ReferenceError: GoodPerson is not defined
类构造函数
使用叫constructor的方法时,解释器将使用new操作符调用此函数以创建实例。定义constructor是可选的,若不定义则默认为空构造函数。
实例化
使用new运算符实例化Person的操作与将new与函数构造函数一起使用的操作相同。唯一可察觉的区别是JavaScript解释器理解将new与类一起使用意味着应将constructor用于实例化。
类实例化必须使用new关键字,使用new调用类构造函数将执行以下操作:
在内存中创建一个新对象。
新对象的内部[[Prototype]]指针被分配为构造函数的prototype属性。
构造函数的this值被分配给新对象(因此在构造函数内部引用时this指向新对象)。
执行构造函数中的代码(向新对象添加属性)。
如果构造函数返回一个对象,则返回该对象。否则,返回刚刚创建的新对象。
类constructor方法并不特殊,类实例化后同平常的实例方法一样(但有相同的构造函数限制),因此可以引用它并延迟实例化:
class Person{}
let p = new Person();
//p.constructor();//TypeError: Class constructor Person cannot be invoked without
'new'
let p2 = new p.constructor();//正常
类是特殊的函数
类标识符有一个prototype属性,该属性有一个constructor属性指向类自身:
class Person{}
const p = new Person();
console.log(typeof p);//object
console.log(typeof Person);//function
console.log(Person.prototype);//{constructor: ƒ}
console.log(Person === Person.prototype.constructor);//true
console.log(p instanceof Person);//true
在类的环境中,当new应用于该类时,该类本身被视为构造函数。当new应用于类内constructor时,与调用普通非类的构造函数相同:
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], 666); // instance 666
类似于立即调用的函数表达式,类也可以立即实例化:
let p = new class Foo {
constructor(x) {
console.log(x);
}
}('666'); // 666
console.log(p); // Foo {}
class Bar {}
let p2 = new Bar();
console.log(p2);//Bar {}
console.log(typeof p2);//object
实例 原型 类成员
实例成员
class Person {
constructor() {
this.name = new String('jack');
this.sayName = () => console.log(this.name);
this.nicknames = ['Joke', 'J-Dog'];
}
}
let p1 = new Person();
let 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(); // Joke
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:'Ciri';//SyntaxError: Unexpected identifier
}
类方法的行为与对象属性相同:
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
类定义支持getter和setter,语法和常规对象相同:
class Person {
set name(newName) {
this.name_ = newName;
}
get name() {
return this.name_;
}
}
let p = new Person();
p.name = 'Ciri';
console.log(p.name); // Ciri
类静态方法和存取器
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 {locate: ƒ}
Person.prototype.locate();//prototype {constructor: ƒ, locate: ƒ}
Person.locate();//class class Person{...}
非函数原型和类成员
尽管类定义不可添加数据属性到类和原型,但在类外面却可以:
class Person{
sayName(){
console.log(`${Person.greeting} ${this.name}`);
}
}
//定义在类上
Person.greeting = 'My name is ';
//定义在原型上
Person.prototype.name = 'Ciri';
let p = new Person();
p.sayName();//My name is
Ciri
迭代器和生成器方法
类定义语法允许在类和原型上定义迭代器和生成器方法:
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
类继承
继承基础
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 {}
构造函数、HomeObjects和super()
派生类的方法通过关键字super获取它们的原型的引用,仅适用于派生类,并且 仅在构造函数或静态方法内部可用 。在构造函数内部使用super来控制何时调用父类构造函数。
class Vehicle {
constructor() {
this.hasEngine = true;
}
}
class Bus extends Vehicle {
constructor() {
// 不能在super()之前引用this,会抛异常
super(); // 同super.constructor()
console.log(this instanceof Vehicle); // true
console.log(this); // Bus { hasEngine: true }
}
}
let bus = new Bus();
super也可用在静态方法中以调用父类静态方法 :
class Vehicle {
static identify() {
console.log('666');
}
}
class Bus extends Vehicle {
static identify() {
super.identify();
}
}
Bus.identify(); // 666
注意:ES6为构造函数和静态方法提供了对内部[[HomeObject]]的引用,此自动分配的指针指向定义该方法的对象。只能在JavaScript引擎内部进行访问, super将始终被定义为[[HomeObject]]的原型。
使用super注意事项:
super只能在衍生类的构造函数和静态方法中使用:
class Vehicle { constructor() { super(); //SyntaxError: 'super' keyword unexpected here } }
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);//Bus {} console.log(this instanceof Vehicle);//true } } new Bus();
super()的行为像极了构造函数,必须手动将参数传递给super(),再由它传递给父构造函数:
class Vehicle { constructor(licensePlate) { this.licensePlate = licensePlate; } } class Bus extends Vehicle { constructor(licensePlate) { super(licensePlate); } } console.log(new Bus('1337H4X'));//Bus {licensePlate: "1337H4X"}
如果不给子类定义构造函数,则将调用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);//ReferenceError: Must call super constructor //in derived class before accessing 'this' or //returning from derived constructor } } new Bus();
如果子类显式定义了构造函数,则必须调用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伪属性在所有函数中可用,在类构造函数中,它指的是构造的类。在普通函数中,它指的是函数本身,假设它是通过new运算符调用的;否则 new.target 是未定义的。在箭头函数中, new.target 是从包围它的作用域继承的。
使用new.target实现抽象基类:
//抽象基类
class Vehicle{
constructor() {
console.log(new.target);
if(new.target === Vehicle){
throw new Error('模拟的抽象基类,不要实例化');
}
}
}
class Bus extends Vehicle{}
new Bus();//class Bus extends Vehicle{}
new Vehicle();//class Vehicle{...}
Error: 模拟的抽象基类,不要实例化
可在抽象基类构造函数中检查子类是否定义了某个方法:
//抽象基类
class Vehicle {
constructor() {
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
if (!this.foo) {
throw new Error('该类木有定义foo()');
} else {
console.log('OK');
}
}
}
class Bus extends Vehicle {
foo() {}
}
class Van extends Vehicle {}
new Bus();//OK
new Van();//Error: 该类木有定义foo()
从内建类型继承
ES6提供与内建引用类型的无缝互操作性:
class SuperArray extends Array {
shuffle() {
// Fisher-Yates 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); // [5, 1, 4, 3, 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 {}
function getParentClass() {
console.log('evaluated expression');
return Vehicle;
}
class Bus extends getParentClass() {}
// evaluated expression
嵌套
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