JavaScript语言基础(五)对象、类与面向对象编程

235 阅读31分钟

关于对象

对象在代码执行过程中的任何时候都可以被创建和增强,具有极大的动态性。

创建自定义对象的通常方式是创建Object的一个新实例,然后给它添加属性和方法。

let person = new Object();
person.name = "jk";
person.age = 23;
person.job = "idol";
person.sayName = function(){
	console.log(this.name);
};

如今,对象字面量变成了更流行的方式:

// 与前面的方式等价
let person = {
	name : "jk",
	age : 23,
	job : "idol",
	sayName(){
		console.log(this.name);
	}
};

属性的类型

ES使用一些内部特性来描述属性的特征,特性时由为JS实现引擎的规范定义的,因此开发者不能在JS中直接访问这些特性

为了将某个特性标识为内部特性,规定会用两个中括号把特性的名称括起来,如 [[ Enumerable ]] 。

属性分为两种:数据属性和访问器属性。

1. 数据属性

包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有4个特性描述它们的行为:

[[Configurable]]:表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。

[[Enumerable]]:表示是否可以通过for-in 循环返回。

[[Writable]]:表示属性的值是否可以被修改。

以上三个特性,在默认情况下,所有直接定义在对象上的属性的这个特征都是true。

[[Value]]:包含属性实际的值。这个特性的默认值为undefined。

要修改属性的默认特性,就必须使用 Object.defineProperty()方法【多数情况下,可能不需要】。这个方法接收3个参数:要给其添加属性的对象、属性的名称和一个描述符对象。最后一个参数即描述符对象上的属性可以包含:configurable、enumerable、writable和value,和以上特性一一对应。根据要修改的特性,可以设置其中一个或多个值。

let person = {};
Object.defineProperty(person, "name", {
	writable : false,	// 表示不可修改属性值
	value : "JJK"
});
console.log(person.name);	// "JJK"
person.name = "lms";	
console.log(person.name);	// "JJK"

在非严格模式下尝试给只读属性重新赋值会忽略;在严格模式下,尝试给只读属性的值会抛出错误。

类似的规则也适用于创建不可配置的属性,即configurable : false。此外,一个属性被定义为不可配置之后,就不能再变回可配置的了。 再次调用Object.defineProperty()并修改任何非writable属性会导致错误。

let person = {};
Object.defineProperty(person, "name", {
	configurable : false,	// 表示不可配置属性值
	value : "JJK"
});

// 抛出错误
Object.defineProperty(person, "name", {
	configurable : true,
	value : "JJK"
});

在调用Object.defineProperty()时,configurable、enumerable和writable的值如果不指定,则都默认为false。

2.访问器属性

包含一个获取(getter)函数和设置(setter)函数,并不是必需的。有四个特性描述它们的行为:

[[Configurable]]:表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。

[[Enumerable]]:表示是否可以通过for-in 循环返回。

以上两个特性,在默认情况下,所有直接定义在对象上的属性的这个特征都是true。

[[Get]]:获取函数,在读取属性时调用,默认值为undefined。

[[Set]]:设置函数,在写入属性时调用,默认值为undefined。

访问器属性时是不能直接定义的,必须使用Object.defineProperty()。

let book = {
	year_ : 2017,	// 伪私有成员,year_中的下划线常用来表示该属性并不希望在对象方法的外部被访问。
	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.edtion);	// 2

这是访问器属性的经典使用场景,即设置一个属性值会导致一些其他变化发生。

获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略,在严格模式下会抛出错误。只有一个设置函数属性是不能读取的,非严格模式下返回undefined,严格模式下抛出错误。

在不支持Object.defineProperty()的浏览器,无法修改[[Configurable]]或[[Enumerable]]。

定义多个属性

ES提供Object.defineProperties()方法,可以通过多个描述符一次性定义多个属性。接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要修改或添加的属性一一对应。

let book = {};
Object.defineProperties(book, {
	year_ : {
		value : 2017
	},
	edition : {
		value : 1
	},

	year : {
		get(){
			return this.year_;
		},
		set(newValue){
			if(newValue > 2017){
				this.year_ = newValue;
				this.edition += newValue - 2017;
			}
		}
	}
});

读取属性的特性

使用Object.getOwnPropertyDescriptor()方法,可以取得指定属性的属性描述符。接收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象,对于访问器属性包含configurable、enumerable、get和set属性,对于数据属性包含configurable、enumerable、writeable和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 des1 = Object.getOwnPropertyDescriptor(book, "year_");
console.log(des1.value);	// 2017
console.log(des1.configurable);	// false
console.log(typeof des1.get);		// "undefined"

let des2 = Object.getOwnPropertyDescriptor(book, "year");
console.log(des2.value);	// undefined
console.log(des2.configurable);	// false
console.log(typeof des2.get);		// "function"

ES 2017新增了Object.getOwnPropertyDescriptors()静态方法,会在每个自有属性上调用Object.getOwnPropertyDescriptor(),并在一个新对象中返回它们。

合并对象

把源对象所有的本地属性一起复制到目标对象上。ES6提供了Object.assign()方法。接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyEnumerable()返回true)和自有(Object.hasOwnProperty()返回true)属性复制到目标对象。 以字符串和符号为键的属性会被复制。

let desc, result;
// 多个源对象
dest = {};
result = Object.assign(dest, {a : 'foo'}, {b : 'bar'});
console.log(result);	// { a: foo, b: bar }

Object.assign() 实际上对每个源对象执行的是浅复制【即只复制对象的引用】。 如果多个源对象都有相同属性,则使用最后一个复制的值。

let dest, src, result;

// 覆盖属性
dest = { id : 'dest' };
result = Object.assign(dest, { id : 'src1', a : 'foo' }, { id : 'src2', b : 'bar'});

// Object.assign()  会覆盖重复的属性
console.log(result);	// {  id : src2, a : foo, b : bar }


// 对象引用
dest = {};
src = { a : {} };

Object.assign(dest , src);

// 浅复制意味着只会复制对象的引用
console.log(dest);	// { a : {} }
console.log(dest.a === src.a);		// true

如果赋值期间出错,则操作中止并退出,同时抛出错误。assign方法没有”回滚“之前赋值的概念,因此它是可能只会完成部分复制的方法。

let dest, src, result;

dest = {};
src = {
	a : 'foo'get b(){	// assign方法在调用获取函数时会抛出错误
		throw new Error();
	},
	c : 'bar'
};

try{
	Object.assign(dest, src);
	catch(e) {};
}

// 只完成部分复制,中止赋值操作并退出,同时抛出错误
console.log(dest);	// { a : foo }

对象标识及相等判定

ES6新增Object.is()方法,与===很像,必须接收两个参数

// 正确的0、+0、-0 相等/不等判定
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

要检查超过两个值,递归地利用相等性传递即可:

function checkFn(x, ...rest){
	return Object.is(x, rest[0]) && (rest.length < 2 || checkFn(...rest));
}

增强的对象语法

1.属性值简写

只要使用变量名(不用再写冒号)就会自动解释为同名的属性键。若没找到同名变量,则会抛出 ReferenceError。

let name = 'JJK';

let person = {
	name
};

console.log(person.name);	// { name : 'JJK' }

代码压缩程序会在不同作用域间保留属性名,以防止找不到引用:

function makePerson(name){
	return{
		name
	};
}
let person = makePerson('lms');
console.log(person.name);	// lms

2.可计算属性

可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为JS表达式,而不是字符串来求值:

const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';

let person = {
	[nameKey] : 'cln',
	[ageKey] : 20,
	[jobKey] : 'singer'
};
console.log(person);	// { name : 'cln', age : 20, job: 'singer'}

因为被当作JS表达式求值,所有可计算属性本身可以是复杂的表达式,在实例化时再求值:

const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;

function getUniqueKey(key){
	return '$(key)_$(uniqueToken++)';
}

let person = {
	[getUniqueKey(nameKey)] : 'cln',
	[getUniqueKey(ageKey)] : 20,
	[getUniqueKey(jobKey)] : 'singer'
};
console.log(person);	// { name_0 : 'cln', age_1 : 20, job_2: 'singer'}

可计算属性表达式中抛出任何错误都会中断对象创建。如果表达式抛出错误,之前完成的计算是不能回滚的。

3.简写方法名

let person = {
	sayName(name){
		console.log('My name is ${name}');
	}
};

person.sayName('jjk');	// My name is jjk

对获取函数和设置函数也是适用的:

get name(){
	return this.name;
},
set name(name){
	this.name_ = name;
}

与可计算属性键相互兼容:

const methodKey = 'sayName';
let person = {
	[methodKey] (name){
		console.log('My name is ${name}');
	}
}
person.sayName('jjk');	// My name is jjk

对象解构[ES新增]

可以在一条语句中使用嵌套数据实现一个或多个赋值操作,即使用与对象匹配的解构来实现对象属性赋值。

使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作。

// 使用对象解构
let person = {
	name : 'jjk',
	age : 23
};
let {	name : personName, age : personAge	} = person;

console.log(personName);	// jjk
console.log(personAge);	// 23

// 可使用简写语法
let {	name , age	} = person;
console.log(name);	// jjk
console.log(age);	//23

解构赋值不一定与对象的属性匹配。赋值时可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是undefined。

let {	name , job	} = person;
console.log(job);	// undefined

也可以在解构赋值的同时定义默认值,适用于前面提到的引用属性不存在于源对象中的情况:

let {	name, job = 'Software enginner'	} = person;

解构在内部使用ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着null和undefined不能被解构(根据ToObject方法的定义),否则抛出TypeError错误。

let {	length	} = 'lms';
console.log(length);	// 3

let {	constructor : c	} = 4;
console.log(c === Number);	// true

let { _ } = null;	// TypeError
let { _ } = undefined;	// TypeError

解构并不要求变量必须在解构表达式中声明。不过,如果是给已经声明的变量赋值,则赋值表达式必须包括在一对括号中

let personName, personAge;

let person = {
	name : 'jjk',
	age : 23
};

( {	name : personName, age : personAge	} = person );

1.嵌套解构

解构对于引用嵌套的属性或赋值目标没有限制,因此可以通过解构来复制对象属性:

let person = {
	name : 'jjk',
	age : 23,
	job : {
		title : 'idol'
	}
};

let personCopy = {};

{	name : personCopy.name, age : personCopy.age, job : personCopy.job	} = person;

// 因为一个对象的引用被赋值给personCopy,所以修改person.job对象的属性也会影响personCopy。
person.job.title = 'dancer';
console.log(personCopy);	// { name : 'jjk', age : 23, job : { title : 'dancer' } }

解构赋值可使用嵌套结构,以匹配嵌套的属性:

// 声明title变量 并将person.job.title的值赋给它。
let {	job : { title }	} = person;
console.log(title);	// idol

在外层属性没有定义的情况下,不能使用嵌套解构,无论源对象还是目标对象都一样。

  1. 部分解构 如果一个解构表达式涉及多个赋值,开始的赋值成功而后面赋值出错,则整个解构赋值只会完成一部分。

  2. 参数上下文匹配 在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响arguments 对象,但可以在函数签名中声明在函数体内使用局部变量:

let person = {
	name : 'jjk',
	age : 23
};

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(name, age);
}

printPerson('1st', person, '2nd');
// ['1st', { name : 'jjk', age : 23 }, '2nd']
// 'jjk', 23

printPerson2('1st', person, '2nd');
// ['1st', { name : 'jjk', age : 23 }, '2nd']
// 'jjk', 23

创建对象

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("JJK",23,"IDOL");
let person2 = createPerson("cln",20,"singer")

这个工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

构造函数模式

前面的例子使用构造函数模式这样写:

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("JJK",23,"IDOL");
let perso2 = new Person("cln",20,"singer");

person1.sayName();		// JJK
person2.sayName();		// cln

两者的区别:没有显示地创建对象,属性和方法直接赋值给了this,没有return。

按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。ES的构造函数就是能够创建对象的函数。

一般用instanceof 操作符来确定对象类型更可靠,前面例子中的每个对象都是Object的实例,同时也是Person的实例。

console.log(person1 instanceof Object); // true

console.log(person1 instanceof Person); // true

定义自定义构造函数可以确保实例被标识为特定类型,相比工厂模式,这是一个很大的好处。

不一定要写成函数声明的形式。赋值给变量的函数表达式也可以表示构造函数:

let Person = function(name,age,job){
	this.name = name;
	this.age = age;
	this.job = job;
	this.sayName = function(){
		console.log(this.name);
	};
}

在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有new操作符,就可调 相应的构造函数。

  1. 构造函数也是函数 任何函数只要使用new操作符调用的就是构造函数,而不使用new操作符调用就是普通函数。
// 作为函数调用
Person("lms", 21, "student");		// 添加到window对象
window.sayName();		// "lms"

在调用一个函数而没有明确设置this值的情况下(即没有作为对象的方法调用,或者没有使用call()/apply()调用),this始终指向Global对象(在浏览器中就是window对象)。

原型模式

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。 实际上,这个对象就是通过调用构造函数创建的对象的原型。

使用原型对象的好处:在它上面定义的属性和方法可以被对象实例共享。

function Person(){}

Person.prototype.name = "JK";	// 所有属性和sayName()方法都直接添加到了Person的prototype属性上。
Person.prototype.age = 23;
Person.prototype.job = "writer";
Person.prototype.sayName = function (){
	console.log(this.name);
};

let p1 = new Person();
let p2 = new Person();

console.log(p1.sayName == p2.sayName());	// true

使用函数表达式也可以:

let person = function() {};

Person.prototype.name = "JK";
...
  1. 理解原型 无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为constructor 的属性,指回与之关联的构造函数。

每次调用构造函数创建一个新实例,这个实例的内部[[ Prototype ]] 指针就会被赋值为构造函数的原型对象。

脚本中没有访问这个[[ Prototype ]] 特性的标准方式,但Firefox、Safari 和 Chrome会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。

实例通过__proto__链接到原型对象,实际上指向隐藏特性[[ Prototype ]]; 构造函数通过prototype 属性链接到原型对象。

实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。

 构造函数有一个prototype属性引用其原型对象,而这个原型对象也有一个constructor属性,引用这个构造函数,即两者循环引用。

function Person() {}
// 声明之后,构造函数就有一个与之关联的原型对象

console.log(typeof Person.prototype);
console.logPerson.prototype);
// {
// constructor : f Person(),
// __proto__: Object
// }

// 构造函数有一个protyotype属性引用其原型对象,而这个原型对象也有一个constructor属性,引用这个构造函数,即两者循环引用。
console.log(Person.protptype.constructor === Person);	//true

// 正常的原型链都会终止于Object的原型对象,Object原型的原型是null
console.log(Person.prototype.__proto__ === Object.prototype);	// true
console.log(Person.prototype.__proto__.constructor === Object);	// true
console.log(Person.prototype.__proto__.__proto__ === null);		// true

console.log(Person.prototype.__proto__);
// {
//	constructor : f Object(),
//	toString : ...
// 	hasOwnProperty:...
//	....
// }

let person1 = new Person(), person2 = new Person();

// 构造函数、原型对象和实例是三种完全不同的对象
console.log(person1 !== Person);	// true
console.log(person1 !== Person.prototype);	// true
console.log(Person.prototype !== Person);	// true

console.log(person1.__proto__ === Person.prototype);	// true
console.log(person1.__proto.construtor === Person);	// true 此处是实例通过__proto__链接到原型对象,再通过constructor引用构造函数

// 同一构造函数创建的两个实例 共享同一个原型对象
console.log(person1.__proto__ === person2.__proto__);	// true

// instanceof 检查实例的原型链中是否包含指定构造函数的原型
console.log(person1 instanceof Person);		// true
console.log(person1 instanceof Object);		// true
console.log(Person.prototype instanceof Object);	// true

可以通过isPrototypeOf()方法确定两个对象之间的这种关系。本质上,这个方法会在传入参数的[[Prototype]]指向调用它的对象时返回true。

console.log(Person.prototype.isPrototypeOf(person1));	// true
console.log(Person.prototype.isPrototypeOf(person2));	// true

ES的Object类型有一个方法叫Object.getPrototypeOf() ,返回参数的内部特性[[Prototype]]的值:

console.log(Object.getPrototypeOf(person1) == Person.prototype);	// true
console.log(Object.getPrototypeOf(person1).name);	// "JJK"

使用此方法可以方便地取得一个对象的原型,而这在通过原型实现继承时显得尤为重要。

Object类型还有一个setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值,可以重写一个对象的原型继承关系。使用此方法可能会严重影响代码性能。

为避免使用Object.setPrototypeOf()可能造成的性能下降,可以通过Object.create()来创建一个新对象,同时为其指定原型:

let biped = {
	numLegs: 2
};
let person = Object.setPrototypeOf(biped);
person.name = "cln";

console.log(Object.getPrototypeOf(person) === biped);	// true
console.log(person.numLengs);	// 2
  1. 原型层级 搜索开始于对象实例本身。如果在这个实例上发现了给定名称,则返回该名称对应的值。如果没有找到,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。

前面提到的constructor 属性只存在于原型对象,因此通过实例对象也是可以访问到的。

虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。若在实例上添加一个与原型对象中同名的属性,那么会在实例上创建这个属性,这个属性会遮住原型对象上的属性

function Person(){}

Person.prototype.name = "jjk";
Person.prototype.age = 23;
Person.prototype.sayName = function(){
	console.log(this.name);
};
let person1 = new Person(),person2 = new Person();

person1.name = "cln";
console.log(person1.name);	// "cln",来自实例
console.log(person2.name);	// "jjk",来自原型

可以使用delete操作符完全删除实例上添加的属性,从而让标识符解析过程能够继续搜索原型对象。

delete person1.name;
console.log(person1.name);	// "jjk",来自原型

hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上,会在属性存在于调用它的对象实例上时返回true

console.log(person1.hasOwnProperty("name"));	// false,此时name=“jjk”来自原型

person1.name = "cln";	// 此时name来自实例
console.log(person1.hasOwnProperty("name"));	// true

delete person1.name;	// 此时name=“jjk”,来自原型
console.log(person1.hasOwnProperty("name"));	// false

** ES的Object.getOwnPropertyDescriptor()方法只对实例属性有效。要取得原型属性的描述符,就必须直接在原型对象上调用Object.getOwnPropertyDescriptor()。**

  1. 原型和in操作符 使用in操作符的两种方式:单独使用和在for-in循环中使用。在单独使用时,in操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上。

如果要确定某个属性是否存在于原型上,则可以同时使用hasOwnProperty()和in操作符:

function hasPrototypeProperty(object, name){
	return !object.hasOwnProperty(name)&& (name in object);
}

只要通过对象可以访问,in操作符就返回true。而hasOwnProperty()只有属性存在于实例上时才返回true。因此只要in操作符返回true且hasOwnProperty()返回false,就说明该属性是一个原型属性。

hasPrototypeProperty()用于确定属性是否在原型上。

let person = new Person();
console.log(hasPrototypeProperty(person, "name"));	// true, name属性首先只存在于原型上

person.name = "lms";	// 此时在实例上重写了这个属性,实例上也有这个属性,实例上的属性遮蔽了原型上的属性。
console.log(hasPrototypeProperty(person, "name"));	// false

在for-in循环中使用in操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。遮蔽原型中不可枚举([[Enumerable]]特性被设置为false)属性的实例属性也会在for-in循环中返回,因为默认时所定义的属性都是可枚举的。

要获取对象上所有可枚举的实例属性,使用Object.keys()方法。接受一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组

let keys = Object.keys(Person.prototype);
console.log(keys);	// "name,age,job,sayName"

let p1 = new Person();
p1.name = "jk";
p1.age = 20;
let p1keys = Object.keys(p1);
console.log(p1keys);	// "[name, age]"

想列出所有实例属性,无论是否可以枚举,都可以使用Object.getOwnPropertyNames()。 返回的数组中包含了一个不可枚举的属性constructor。以上两种方法在适当的时候都可以用来代替for-in循环。

在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) ]
  1. 属性枚举顺序 for-in循环和Object.keys()的枚举顺序是不确定的,取决于JavaScript引擎,可能因浏览器而异。

Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()的枚举顺序是确定性的。 先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。

let k1 = Symbol('k1'), k2 = Symbol('k2');

let o = {
	1 : 1,
	first : 'first',
	[k1] : 'sym2',
	second : 'second',
	0 : 0
};

o[k2] = 'sym2';	// 添加枚举元素
o[3] = 3;
o.third = 'third';
o[2] = 2;

console.log(Object.getOwnPropertyNames(o));
// ["0","1","2","3","first","second","third"]

console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]

对象迭代

ES2017新增两个静态方法:都接受一个对象,Object.values()返回对象值的数组,Object.entries()返回键/值对的数组。

非字符串属性会被转换为字符串输出。两个方法执行对象的浅复制。且符号属性会被忽略:

const = {
	qux : {} 
};
console.log(Object.values(o) [0] === o.qux);	// true
console.log(Object.entries(o) [0] [1] === o.qux);	// true

const sym = Symbol();
const a = {
	[sym] : 'foo'
};
console.log(Object.values(a));	// []
console.log(Object.entries(a));	// []
  1. 其它原型语法 通过一个包含所有属性和方法的对象字面量来重写前面例子的原型:
function Person(){}

Person.prototype = {
	constructor : Person,
	name : "jk",
	age : 23,
	job : "idol",
	sayName(){
		console.log(this.name);
	}
};

需注意,这种方式恢复constructor属性会创建一个[[Enumerable]]为true的属性,而原生constructor属性默认是不可枚举的。

如果使用的是兼容ES的JS引擎,可以改为使用Object.defineProperty()方法来定义constructor属性:

function Person(){}

Person.prototype = {
	name : "jk",
	age : 23,
	job : "idol",
	sayName(){
		console.log(this.name);
	}
};

// 恢复constructor属性
Object.defineProperty(Person.prototype, "constructor"){
	enumerable : false,
	value : Person
};
  1. 原型的动态性 即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。

虽然随时能给原型添加属性和方法,并能够立即反映在所有对象实例上,但跟重新整个原型是两回事。 实例的[[Prototype]] 指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同的对象也不会变。 重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。 记住,实例只有指向原型的指针,没有指向构造函数的指针。

function Person(){}

let friend = new Person();
Person.prototype = {
	name : "jk",
	age : 23,
	job : "idol",
	sayName(){
		console.log(this.name);
	}
};

friend.sayName();	// 错误,因为此时friend实例引用的是最初的原型,并没有sayName()方法

重新构造函数上的原型之后再创建的实例才会引用新的原型。而在此之前创建的实例仍然会引用最初的原型。

  1. 原生对象原型 原型模式之所以重要,在于自定义类型上,它也是实现所有原生引用类型的模式。 所有原生引用类型的构造函数(包括Object、Array、String等)都在原型上定义了实例方法。 如数组实例的sort()是在Array.prototype上定义的,而字符串包装对象的substring()也是在String.prototype上定义的。

通过原生对象的原型可以获得所有默认方法的引用,也可以给原生类型的实例定义新的方法。

String.prototype.startsWith = function(text){
	return this.indexOf(text) === 0;
};

let msg = "Hello world!";
console.log(msg.startsWith("Hello"));	// true

不推荐在产品环境中修改原生对象原型,可能引发命名冲突。推荐创建一个自定义的类,继承原生类型。

  1. 原型的问题 原型上的所有属性是在实例间共享的,对函数来说比较合适。

在一般情况下,不同的实例应该有属于自己的属性副本,这就是实际开发中通常不单独使用原型模式的原因。

继承

很多面向对象语言都支持两种继承:接口和实现继承,前者只继承方法前面,后者继承实际的方法。 接口继承在ES中是不可能的,因为函数没有签名实现继承是ES唯一支持的继承方式,主要是通过原型链实现的。

原型链

基本思想:通过原型继承多个引用类型的属性和方法。

重温构造函数、原型和实例的关系: 每个构造函数都有一个原型对象,原型就有一个属性constructor指回构造函数,而实例有一个内部指针指向原型。

如果原型是另一个类型的实例,意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。 这样就在实例和原型之间构造了一条原型链,就是原型链的基本构想。

function SuperType(){
	this.property = true;
}
SuperType.prototype.getSuperValue = function(){
	return this.property;
};

function SubType(){
	this.subproperty = false;
}

SubType.prototype = new SuperType();		// 继承SuperType
SuperType.prototype.getSubValue = function(){
	return this.subproperty;
};

let instance = new SubType();
console.log(instance.getSuperValue);	// true

SubType没有使用默认原型,而是将其替换成了一个新的对象,这个对象恰好是SuperType的实例。 这样一来,SubType的实例不仅能从SuperType的实例中继承属性和方法,而且还能与SuperType的原型挂上钩。

注意,getSuperValue()是一个原型方法,而property是一个实例属性,因此property存储在SubType.prototype上面。 由于SubType.prototype的constructor属性被重写为指向SuperType,所以instance.constructor也指向SuperType。

原型链扩展了前面的原型搜索机制。对属性和方法的搜索会一直持续到原型链的末端。

  1. 默认原型 默认情况下,所有引用类型都继承自Object,这也是通过原型链实现的。任何函数的默认原型都是一个Object的实例,意味着这个实例有一个内部指针指向Object.prototype。

SubType继承SuperType,而SuperType继承Object。在调用instance.toString()时,实际上调用的是保存在Object.prototype上的方法。

  1. 原型与继承关系 原型与实例的关系通过两种方式来确定: instanceof操作符 如果一个实例的原型链中出现过相应的构造函数,则instanceof返回true。
instance instanceof SubType		// true
instance instanceof SuperType	// true
instance instanceof Object		// true

isPrototypeOf()方法 原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,就返回true。

Object.prototype.isPrototypeOf(instance)	// true
SuperType.prototype.isPrototypeOf(instance)	// true
SubType.prototype.isPrototypeOf(instance)	// true
  1. 关于方法 子类有时候需要覆盖父类的方法,或增加父类没有的方法。这些方法必须在原型赋值之后再添加到原型上。
function SuperType(){
	this.property = true;
}
SuperType.prototype.getSuperValue = function(){
	return this.property;
};

function SubType(){
	this.subproperty = false;
}

SubType.prototype = new SuperType();		// 继承SuperType
// 新方法
SuperType.prototype.getSubValue = function(){
	return this.subproperty;
};

// 覆盖已有的方法
SubType.prototype.getSuperValue = function(){
	return false;
}

let instance = new SubType();
console.log(instance.getSuperValue);	// false

重点在于上述两个方法都是在把原型赋值为SuperType的实例之后定义的。

以对象字面量方式创建原型方法会破坏之前的原型链,相当于重写了原型链。

// 继承SuperType
SubType.prototype = new SuperType();	

// 通过对象字面量添加新方法,会导致上一行无效
SubType.prototype = {
	getSubValue(){
		return this.subproperty;
	};

	someMethods(){
		return false;
	}
};

let instance = new SubType();
console.log(instance.getSuperValue);	// 出错!!

覆盖后的原型是一个Object的实例,而不再是SuperType的实例,因此之前的原型链就断了。

  1. 原型链的问题 由于原型中包含的引用值会在所有实例间共享,在使用原型实现继承时,原型实际上变成了另一个类型的实例,意味着原先的实例属性摇身一变成为了原型属性。

第二个问题是,子类型在实例化时不能给父类型的构造函数传参。

由于以上的问题,导致原始链基本不会被单独使用。

盗用构造函数(又称“对象伪装”或“经典传承”)

为解决原型包含引用值导致的继承问题的技术。 基本思路:在子类构造函数中调用父类构造函数。 因为函数是在特定上下文中执行代码的简单对象,所以可以使用apply()和call()方法以新创建的对象为上下文执行构造函数。

function SuperType(){
	this.colors  = ["red""blue"];
}
function SubType(){
	SuperType.call(this);	// 继承SuperType
}
let a = new SubType();
a.colors.push("green");
console.log(a.colors);	// "red,blue,green"

let b = new SubType();
console.log(b.colors);	// "red,blue"
  1. 传递参数 相比于使用原型链,盗用构造函数的一个优点是可以在子类构造函数中向父类构造函数传参
function SuperType(name){
	this.name = name;
}
function SubType(){
	SuperType.call(this, "jk");	// 继承并传参
	this.age = 22;	// 实例属性
}
let a = new SubType();
console.log(a.name);	// "jk"
console.log(a.age);		// 22
  1. 盗用构造函数的问题 必须在构造函数中定义方法,因此函数不能重用。

子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。

由于以上的问题,盗用构造函数基本上不能单独使用。

组合继承(也称“伪经典继承”)【使用最多的继承模式

综合原型链和盗用构造函数,基本思路:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。

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 a = new SubType("lms",21);
a.colors.push("grey");
console.log(a.colors);	// "red,blue,green,grey"
a.sayName();		// "lms"
a.sayAge();		// 21

let b = new SubType("jjk",23);
console.log(b.colors);	// "red,blue,green"
b.sayName();		// "jk"
b.sayAge();		// 23

组合继承弥补了原型链和盗用构造函数的不足,是JS中使用最多的继承方式。

原型式继承

即使不自定义类型也可以通过原型实现对象之间的信息共享。

let person = {
	name : "jk",
	friends : ["v","jimin"]
};

let person1 = object(person);
person1.name = "Mary";
person1.friends.push("john");

let person2 = object(person);
person2.name = "Jack";
person2.friends.push("cln");

console.log(person.friends);	// "v, jimin, john, cln"

原型式继承适用这种情况:你有一个对象,想在它的的基础上再创建一个新对象,需要把这个对象先传给object(),然后在对返回对象进行适当修改。

ES5通过增加Object.create()方法将原型式继承的概念规范化了。接受两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。 在只有一个参数时,与object()方法效果相同。第二个参数与Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述,以这种方式添加的属性都会遮蔽原型对象上的同名属性。

let person = {
	name : "jk",
	friends : ["v","jimin"]
};

let person1 = object.create(person, {
	name : {
		value : "cln"
	}
});
console.log(person1.name);	// "cln"

原型式继承非常适合**不需要单独创建构造函数,但仍需在对象间共享信息的场合。**但要记住,属性中包含的引用值始终会在相关对象间共享,和使用原型模式一样的。

寄生式继承

与原型式继承比较接近。基本思路:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

// 基本的寄生继承模式
function createAnother(original){
	let clone = object(original);	// 通过调用函数创建一个新对象
	clone.sayHi = function(){	// 以某种方式增强这个对象
		console.log("hi");
	}
	return clone;	
}

let person = {
	name : "jk",
	friends : ["v","jimin"]
};

let person1 = createAnother(person);
person1.sayHi();	// "hi"

同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的,任何返回新对象的函数都可在这使用。

通过这种方式给对象添加函数会导致函数难以重用,与构造函数模式类似。

寄生式组合继承

通过盗用构造函数继承属性,但使用混合式原型链继承方法。

基本思路:取得父类原型的一个副本,即使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function inheritPrototype(subType, superType){
	let prototype = object(superType.prototype);	// 创建对象
	prototype.constructor = SubType;		// 增强对象
	subType.prototype = prototype;		// 赋值对象
}

inheritPrototype()接收2个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本, 然后给返回的prototype对象设置constructor属性指向子类构造函数,最后将新创建的对象赋值给子类型原型。

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原型上不必要也用不到的属性,这个例子效率更高,而且原始链仍然保持不变。寄生式组合继承可以算是引用类型继承的最佳模式。

类 【ES6新增】

ES6新引入的class关键字具有正式定义类的能力。类是ES中新的基础性语法糖结构。实际上背后使用的仍然是原型和构造函数的概念。

类定义

与函数类型相似,定义类有两种主要方式:

  1. 类声明

class Person{}

  1. 类表达式

const Animal = class{};

与函数表达式相似,类表达式在它们被求值前也不能引用。但是与函数定义不同的是,虽然函数声明可以提升,但类定义不能(即类是在定义之后才能使用的)。

console.log(FunctionDeclaration);	// FunctionDeclaration(){}
function FunctionDeclaration(){};
console.log(FunctionDeclaration);	// FunctionDeclaration(){}

console.log(ClassDeclaration);	// ReferenceError: ClassDeclaration is not defined
class ClassDeclaration{};
console.log(ClassDeclaration);	// ClassDeclaration(){}

另一个与函数声明不同的地方是,函数受函数作用域限制,类受块作用域限制

{
	function FunctionDeclaration() {}
	class ClassDeclaration {}
}
console.log(FunctionDeclaration);	// FunctionDeclaration(){}
console.log(ClassDeclaration);		// ReferenceError: ClassDeclaration is not defined

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这都不是必需的。建议类名的首字母要大写。

类构造函数

构造函数的定义不是必需的,不定义构造函数想当于将构造函数定义为空函数。

  1. 实例化 constructor关键字用于在类定义块内部创建类的构造函数。
class Person {
	constructor(){
		console.log('person ctor');
	}
}
let p = new Person();	// person ctor

类实例化时传入的参数会用作构造函数的参数。若不需要参数,则类名后面的括号是可选的。

类构造函数与构造函数的主要区别是,调用类构造函数必须使用new操作符;而普通的如果不使用new,就会以全局的this(通常是window)作为内部对象。调用类构造函数时如果忘了使用new则会抛出错误。

类构造函数没有什么特殊之处,实例化之后,它会变成普通的实例方法。实例化之后,可以在实例上引用它:

class Person{}
let p1 = new Person{};
p1.constructor();	// TypeError
let p2 = new p1.constructor();	// 使用对类构造函数的引用,创建一个新实例
  1. 把类当成特殊函数 ES中没有正式的类这个类型。从各方面看,ES类就是一种特殊函数。声明一个类之后,通过typeof操作符检测类标识符,表明它是一个函数:
class Person{}

console.log(Person);	// class Person{}
console.log(typeof Person);	// function

类标识符有prototype属性,而这个原型也有一个constructor属性指向类自身:

class Person{}
console.log(Person.prototype);	// {constructor : f() }
console.log(Person.prototype.constructor === Person);	// true

与普通构造函数一样,可以使用instanceof操作符检查一个对象与类构造函数,以确定这个对象是不是类的实例。

类可以像其他对象或函数引用一样把类作为参数传递:

// 类可以像函数一样在任何地方定义,比如在数组中
let classList = [
	class{
		constructor(id){
			this.id_=id;
			console.log('instance ${this.id_}');
		}
	}
];
function createInstance(classDefination, id){
	return new classDefination(id);
}
let foo = createInstance(classList[0], 3141);	// instance 3141

与立即调用函数表达式相似,类可以立即实例化:

// 是类表达式,所以类名是可选的
let p = new class Foo{
	constructor(x){
		console.log(x);
	}
}('bar');	// bar

console.log(p);	// Foo{}

实例、原型和类成员

1.实例成员

每次通过new调用类标识符时,都会执行类构造函数。在这个函数内部,可以为新创建的实例(this)添加“自有”属性。在构造函数执行完毕后,仍然可以给实例继续添加新成员。 每个实例都对应一个唯一的成员对象,意味着所有成员都不会在原型上共享

class Person{
	constructor(){
		this.name = new String('Jack');
		this.sayName = () => console.log(this.name);
		this.nicknames = ['Jake', 'J-Dog']
	}
}

let p1 = new Person{},p2 = new Person{};
p1.sayName();	// Jack
p2.sayName();	// Jack

console.log(p1.name === p2.name);		// false
console.log(p1.sayName === p2.sayName);	// false
console.log(p1.nicknames === p2.nicknames);	// false

p1.name = p1.nicknames[0];
p2.name = p2.nicknames[1];

p1.sayName();	// Jake
p2.sayName();	// J-Dog

2.原型方法与访问器

为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。

class Person{
	constructor(){
		this.locate = () => console.log('instance');
	}
	// 在类块中定义的所有内容都会定义在类的原型上
	locate(){
		console.log('prototype');
	}
}
let p = new Person();
p.locate();			// instance
Person.prototype.locate();	// prototype

可以把方法定义在类构造函数/类块中,但不能在类块中给原型添加原始值/对象作为成员数据

class Person{
	name : 'jjk'	// Uncaught SyntaxError
}

类的方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键:

const symbolKey = Symbol('sym');
class Person{
	[symbolKey](){
		console.log('invoked symbolKey');
	}
	['computed' + 'Key'](){
		console.log('invoked computedKey');
	}
}
let p = new Person();
p.symbolKey();	// invoked symbolKey
p.computedKey();	// invoked computedKey

类定义也支持获取和设置访问器,语法与行为跟普通对象一样。

class Person{
	set name(newName){
		this.name_ = name;
	}

	get name(){
		return this.name_;
	}
}

3.静态类方法

通常用于执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,静态成员每个类上只能有一个

class Person{
	constructor(){
		// 添加到this的所有内容都会存在于不同的实例上
		this.locate = () => console.log('instance', this);
	}
	// 定义在类的原型对象上
	locate(){
		console.log('prototype', this);
	}

	// 定义在类本身上
	static locate(){
		console.log('class', this);
	}
}

静态类方法非常适合作为实例工厂:

static create(){
	// 使用随机年龄创建并返回一个Person实例
	return new Person(Math.floor(Math.random()*100));
}

4.非函数原型和类成员

虽然类定义不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加

class Person{
	sayName(){
		console.log('$(Person.greeting) $(this.name)');
	}
}

Person.greeting = 'My name is';	// 在类上定义数据成员
Person.prototype.name = 'CLN';	// 在原型上定义数据成员

let p = new Person();
p.sayName();	// My name is CLN

5. 迭代器与生成器方法

类定义语法支持在原型和类本身上定义生成器方法。

class Person{
	*createNicknameIterator(){	// 在原型上定义生成器方法
		yield 'nn';
		yield 'jk';
		yield 'cln';
	}

	static *createJobIterator(){	// 在类上定义生成器方法
		yield 'idol';
		yield 'teacher';
		yield 'boss';
	}
}

let job = Person.createJobIterator();
console.log(job.next().value);	// idol
console.log(job.next().value);	// teacher
console.log(job.next().value);	// boss

let p = new Person();
let nick = p.createNicknameIterator();
console.log(nick.next().value);	// nn
console.log(nick.next().value);	// jk
console.log(nick.next().value);	// cln

因为支持生成器方法,可以通过添加一个默认迭代器,把类实例变成可迭代对象:

class Person{
	constructor(){
		this.nicknames = ['nn', 'jk', 'cln'];
	}
	*[Symbol.iterator]{
		yield *this.nicknames.entries();
	}
}

let p = new Person();
for(let [idx, nickname] of p){
	console.log(nickname);
}

也可以只返回迭代器实例

class Person{
	constructor(){
		this.nicknames = ['nn', 'jk', 'cln'];
	}
	[Symbol.iterator]{
		yield this.nicknames.entries();
	}
}

继承

虽然类继承使用的是新语法,但背后依旧使用的是原型链。

  1. 继承基础 ES6类支持单继承。使用extends关键字,就可以继承任何拥有[[ Construct ]]和原型的对象。 很大程度上,意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)
class Vehicle{}
class Bus extends Vehicle{}	// 继承类

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.identifyClass('vehicle');	// vehicle, Vehicle{}

Bus.identifyPrototype('bus');	// bus, class Bus{}
Vehicle.identifyClass('vehicle');	// vehicle, class Vehicle{}

extends 关键字也可以在类表达式中使用,因此let Bar = class extends Foo{}是有效的。

  1. 构造函数、HomeObject和super() 派生类的方法可以通过super关键字引用它们的原型,只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。 在类构造函数中使用super可以调用父类构造函数。
class Vehicle{
	constructor(){
		this.hasEngine = true;
	}
}
class Bus extends Vehicle{
	constructor(){
		// 不要在调用super()之前引用this,否则抛出ReferenceError。
		super();
		console.log(this instanceof Vehicle);	// true
		console.log(this);	// Bus{ hasEngine = true }
	}
}
new Bus{};

在静态方法中可以通过super调用继承的类上定义的静态方法:

class Vehicle{
	static identify(){
		console.log('vehicle static function');
	}
}

class Bus extends Vehicle{
	static identify(){
		super.identify();
	}
}
Bus.identify();	// vehicle static function

ES6给类构造函数和静态方法添加了内部特性[[HomeObject]], 此特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在JS引擎内部访问。super始终会定义为[[HomeObject]]的原型。

使用super时需要注意:

  1. super只能在派生类构造函数和静态方法中使用;

  2. 不能单独使用super关键字,要么调用构造函数,要么引用静态方法;

constructor(){
	console.log(super);
	// SyntaxError : 'super' keyword unexpected here
}
  1. 调用super()会调用父类构造函数,并将返回的实例赋值给this;

  2. super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需手动传入。

class Vehicle{
	constructor(license){
		this.license = license;
	}
}
class Bus extends Vehicle{
	constructor(license){
		super(license);
	}
}
console.log(new Bus('d8v2d'));	// Bus{ license: 'd8v2d' }
  1. 若没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数。
class Vehicle{
	constructor(license){
		this.license = license;
	}
}
class Bus extends Vehicle{}
console.log(new Bus('d8v2d'));	// Bus{ license: 'd8v2d' }
  1. 在类构造函数中,不能在调用super()之前引用this。

  2. 若在派生类中显示定义了构造函数,则要么必须在其中调用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())	// {}
  1. 抽象基类 供其他类继承,但本身不会被实例化。ES中没有专门支持这种类的语法,但可以通过new.target来实现。 new.target保存通过new关键字调用的类或函数。通过在实例化时检测new.target是否为抽象基类,可以阻止对抽象基类的实例化。
class Vehicle{		// 抽象基类
	constructor(){
		console.log(new.target);
		if(new.target === Vehicle){
			throw new Error('Vehicle cannot be directly instantiated');
		}
	}
}
class Bus extends Vehicle{};	// 派生类
new Bus();	// class Bus{}
new Vehicle();	// class Vehicle{}
// Error: Vehicle cannot be directly instantiated

通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。

class Vehicle{
	constructor(){
		if(new.target === Vehicle){
			throw new Error('Vehicle cannot be directly instantiated');
		}
		if(!this.foo){
			throw new Error('Inheriting class must define foo()');
		}
		console.log('success!!');
	}
}
class Bus extends Vehicle{
	foo(){}
}
class Van extends Vehicle{}

new Bus();	// success!!
new Van();	// Error: Inheriting class must define foo()
  1. 继承内置类型
class SuperArray extends Array{
	....
}

有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的。 若想覆盖这个默认行为,则可以覆盖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 instanceof SuperArray);	// true
console.log(a2 instanceof SuperArray);	// false
  1. 类混入 把不同类的行为集中到一个类是一种常见的JS模式。

注意,Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。 若只是需要混入多个对象的属性,使用Object.assign()即可。

extends 关键字后面是一个JavaScript表达式。任何可以解析为一个类或一个构造函数的表达式都是有效的。这个表达式会在求值类定义时求值:

class Vehicle{}
function getParentClass(){
	console.log('evaluated expression');
	return Vehicle;
}
class Bus extends getParentClass(){}
// 可求值的表达式

混入模式可以通过一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类。

实现此模式的策略: 定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式:

class Vehicle{}
let FooMixin = (Superclass) => class extends Superclass{
	foo(){
		console.log('foo');
	}
}
let BarMixin = (Superclass) => class extends Superclass{
	bar(){
		console.log('bar');
	}
}
let ClnMixin = (Superclass) => class extends Superclass{
	cln(){
		console.log('cln');
	}
}

class Bus extends FooMinxin(BarMixin(ClnMixin(Vehicle))) {}

let b = new Bus();
b.foo();	// foo
b.bar();	// bar
b.cln();	// cln

很多JS框架(特别是React)已经抛弃混入模式,转向了组合模式(把方法提取到独立地类和辅助对象中,然后把它们组合起来,但不使用继承)。 "组合胜过继承"这个设计原则被很多人遵循,在代码涉及中能提供极大的灵活性。