ECMA-262 将对象定义为一组属性的无序集合。即对象是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,该名称映射到一个值。
创建自定义对象的通常方式是创建Object的一个新实例,然后再给它添加属性和方法。也可以通过对象字面量方法创建。
ECMA-262使用一些内部特性来描述属性的特征,开发者不能在JS中直接访问这些特性,为了将某个特性标识为内部特性,会用中括号将特性名称括起来。
数据属性包含一个保存数据值的位置。值会从这个位置读取和写入。数据属性有 4个特性描述它们的行为。
要修改属性的默认特性,就必须使用Object.defineProperty()方法,接收3个参数:要给其添加属性的对象、属性名称和描述符对象。描述符对象属性可包含:configurable、enumerable、writable和value
访问器属性不包含数据值。相反,它们包含一个getter函数和一个setter函数,二者都非必须。访问器属性有 4 个特性描述它们的行为。
访问器属性不能直接定义,必须使用Object.defineProperty()
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
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;
}
}
}
});
let descriptor = Object.getOwnPropertyDescriptor(book, 'year_');
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, 'year');
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"
console.log(Object.getOwnPropertyDescriptors());
// {
// edition: {
// configurable: false,
// enumerable: false,
// value: 1,
// writable: false
// },
// year: {
// configurable: false,
// enumerable: false,
// get: f(),
// set: f(newValue),
// },
// year_: {
// configurable: false,
// enumerable: false,
// value: 2017,
// writable: false
// }
// }
let dest, src, result;
/**
* 简单复制
*/
dest = {};
src = { id: 'src' };
result = Object.assign(dest, src);
// Object.assign修改目标对象,返回修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: 'src' }
console.log(dest); // { id: 'src' }
/**
* 多个源对象
*/
dest = {};
result = Object.assign(dest, { a: 'foo' }, { b: 'bar' });
console.log(result); // { a: 'foo', b: 'bar' }
/**
* 获取函数与设置函数
*/
dest = {
set a(val) {
console.log(`Invoked dest setter with parm ${val}`);
}
};
src = {
get a() {
console.log('Invoked src getter');
return 'foo';
}
};
Object.assign(dest, src);
// 调用src的获取方法
// 调用dest的设置方法,并传入参数'foo'
// 因为此处设置函数不执行赋值操作,所以实际上并没有把值转移过来
console.log(dest); //
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: 'src1', a: 'foo', b: 'bar' }
// 可以通过在目标对象上的设置函数观察到覆盖过程
dest = {
set id(x) {
console.log(x);
}
};
Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'third' });
// first second third
/**
* 对象引用
*/
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a: {} }
console.log(dest.a === src.a); // true
/**
* 错误处理
*/
dest = {};
src = {
a: 'foo',
get b() {
// Object.assign() 在调用这个获取函数时会抛出错误
throw new Error();
},
c: 'bar'
};
try {
Object.assign(dest, src);
} catch (error) {}
// Object.assign() 没办法回滚已经完成的修改
// 在出错之前,目标对象上已经完成的修改会继续存在
console.log(dest); // { a: 'foo' }
在ES6之前,有些特殊情况即使===操作符也无能为力。为此,ES6新增了Object.is(),该方法与===很像,但同时考虑一些特殊情况。该方法接收两个比较参数,要比较多个可以递归地利用相等性传递即可。
// === 符合预期的情况
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false
// 这些情况在不同JS引擎中表现不同,但被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 要确定NaN的相等性,必须使用极为讨厌的isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正确的额0、-0、+0相等/不相等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正确的NaN相等判定
console.log(Object.is(NaN, NaN)); // true
// 要检查超过两个值,递归地利用相等性传递即可
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length< 2 || recursivelyCheckEqual(...rest));
}
ES6为定义和操作对象新增了很多极其有用的语法糖特性。这些特性没有改变现有引擎的行为,但极大提升了处理对象的方便程度。
let name = 'Matt';
let person = { name };
console.log(person); // { name: 'Matt' }
// 代码压缩程序会在不同作用域键保留属性名,以防止找不到引用
function makePerson(name) {
return {
name
};
}
let person2 = makePerson('Matt');
console.log(person2.name); // Matt
在引入可计算属性之前,如果使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {};
person[nameKey] = 'Matt';
person[ageKey] = 27;
person[jobKey] = 'Software engineer';
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为JS表达式而不是字符串来求值
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
[nameKey]: 'Matt',
[ageKey]: 27,
[jobKey]: 'Software engineer'
};
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
// 使用复杂表达式,在实例化时再求值
let uniqueToken = 0;
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`;
}
let person2 = {
[getUniqueKey(nameKey)]: 'Matt',
[getUniqueKey(ageKey)]: 27,
[getUniqueKey(jobKey)]: 'Software engineer'
};
console.log(person2); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
// 方法名 冒号 匿名函数表达式
let person = {
sayName: function(name) {
console.log(`My name is ${name}`);
}
};
// 简写
let person2 = {
sayName(name) {
console.log(`My name is ${name}`);
}
};
// 简写方法名对获取函数和设置函数也适用
let person3 = {
name_: '',
get name() {
return this.name_;
},
set name(name) {
this.name_ = name;
},
sayName() {
console.log(`My name is ${this.name_}`);
}
}
// 简写方法名与可计算属性键相互兼容
const methodKey = 'sayName';
let person4 = {
[methodKey](name) {
console.log(`My name is ${name}`);
}
};
ES6新增对象结构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。即对象结构就是使用与对象匹配的结构来实现对象属性赋值
let person = {
name: 'John',
age: 18
};
let { name: personName, age: personAge } = person;
console.log(personName); // John
console.log(personAge); // 18
// 如果让变量直接使用属性的名称,可以使用简写语法
let { name, age } = person;
// 解构赋值不一定与对象的属性匹配,赋值的时候可以忽略某些属性,如果引用的属性不存在,则该变量的值就是undefined
let { job } = person;
console.log(job); // undefined
// 也可以在解构赋值的同时定义默认值
let { gender = "male" } = person;
console.log(gender); // male
// 解构在内部使用函数ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象
// 即在对象解构上下文中,原始值会被当成对象,也就说null和undefined不能被解构,否则会抛出错误
let { length } = 'foobar';
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
// 如果给事先声明的变量赋值,则赋值表达式必须包含在一对括号中
let personAddress, personEmail;
let person2 = {
address: 'Beijing',
email: 'a@example.cn'
};
({ address: personAddress, email: personEmail } = person2);
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
let personCopy = {};
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person);
// 因为一个对象的引用被赋值给personCopy,修改该对象对copy对象也有影响
person.job.title = 'Hacker';
console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
// 解构赋值可以使用嵌套解构,以匹配嵌套的属性
let { job: { title }} = person;
console.log(title); // Hacker
// 在外层属性没有定义的情况下不能使用嵌套解构,无论源对象还是目标对象都一样
// 涉及多个属性的解构赋值是一个输出无关的顺序化操作
// 如果一个解构表达式涉及多个赋值,开始的赋值成功而后面出错,则只会完成一部分
let person = {
name: 'Matt',
age: 27,
};
let personName, personBar, personAge;
try {
// person.foo 是 undefined,因此会抛出错误
({ name: personName, foo: { bar: personBar }, age: personAge } = person);
} catch (e) {}
console.log(personName, personBar, personAge);
// Matt, undefined, undefined
在函数参数列表中也可以进行解构赋值,对参数的解构不会影响arguments对象,但可以在函数签名中声明在函数体内使用局部变量
let person = {
name: 'Matt',
age: 27
};
function printPerson(foo, {name, age}, bar) {
console.log(arguments);
console.log(name, age);
}
function printPerson2(foo, { name: personName, age: personAge}, bar) {
console.log(arguments);
console.log(personName, personAge);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
printPerson('2nd', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
ES6开始正式支持类和继承。ES6的类旨在完全涵盖之前规范设计的基于原型的继承模式。ES6的类仅仅是封装了ES5.1构造函数加原型继承的语法糖而已。
// 工厂模式用于抽象创建特定对象的过程
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 p1 = createPerson('Nicholas', 29, 'Software engineer');
let p2 = createPerson('Greg', 27, 'Doctor');
工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)
ECMAScript中的构造函数是用于创建特定类型对象的。像Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用。也可以自定义构造函数,以函数形式为自己的对象类型定义属性和方法。
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');
// Person()构造函数代替了createPerson()工厂函数。实际上,Person()内部的代码跟createPerson()基本是一样的
// person1和person2分别保存着Person的不同实例,这两个对象都有一个constructor属性指向Person
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
// constructor本来是用于表示对象类型的。不过,一般认为instanceof操作符是确定对象类型更可靠的方式
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 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);
};
}
let person1 = new Person('Nicholas', 29, 'Software engineer');
let person2 = new Person('Greg', 27, 'Doctor');
person1.sayName(); // Nicholas
person2.sayName(); // Greg
function Person() {
this.name = "Jake";
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person();
let person2 = new Person;
person1.sayName(); // Jake
person2.sayName(); // Jake
// 作为构造函数
ley 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 Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)"); // 逻辑等价
}
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
// 此处sayName() 被定义在了构造函数外部,在函数内部,sayName属性等于全局sayName()函数。
// 因为sayName属性中包含的只是一个指向外部函数的指针,所以p1和p2共享了定义在全局作用域上的sayName()函数
// 但是这种方案会导致全局作用域被搞乱,因为该函数只能在一个(类?)对象上调用,这个问题可以通过原型模式解决
let p1 = new Person('Nicholas', 29, 'Software engineer');
let p2 = new Person('Greg', 27, 'Doctor');
p1.sayName(); // Nicholas
p2.sayName(); // Greg
每个函数都会创建一个prototype属性,该属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,该对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。
function Person() {}
// 也可以使用函数表达式 如 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 p1 = new Person();
p1.sayName(); // "Nicholas"
let p2 = new Person();
p2.sayName(); // "Nicholas"
console.log(p1.sayName == p2.sayName); // true
理解原型
/**
* 构造函数可以是函数表达式
* 也可以是函数声明
* function Person() {}
* let Person = function() {}
*/
function Person() {}
/**
* 声明之后,构造函数就有一个与之关联的原型对象
*/
console.log(typeof Person.prototype);
console.log(Person.prototype);
// object
// {
// constructor: f Person(),
// __proto__: Object
//}
/**
* 构造函数有一个prototype属性,引用其原型对象
* 而这个原型对象也有一个constructor属性,引用这个构造函数
* 即两者循环引用
*/
console.log(Person.prototype.constructor === Person); // true
/**
* 正常的原型链都会终止于Object的原型对象
* Object原型的原型是null
*/
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
console.log(Person.prototype.__proto__);
// {
// constructor: f Object(),
// toString: ...
// hasOwnProperty: ...
// isPrototypeOf: ...
// ...
//}
let p1 = new Person();
let p2 = new Person();
/**
* 构造函数、原型对象和实例是3个完全不同的对象
*/
console.log(p1 !== Person); // true
console.log(p1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
/**
* 实例通过__proto__链接到原型对象,实际上指向隐藏特性[[Prototype]]
* 构造函数通过prototype属性链接到原型对象
* 实例与构造函数没有直接联系,与原型对象有直接联系
*/
console.log(p1.__proto__ === Person.prototype); // true
console.log(p1.__proto__.constructor === Person); // true
/**
* 同一个构造函数创建的两个实例,共享一个原型对象
*/
console.log(p1.__proto__ === p2.__proto__); // true
/**
* instanceof 检查实例的原型链中是否也包含指定构造函数的原型
*/
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Object); // true
console.log(p1.prototype instanceof Object); // true
console.log(Object.getPrototypeOf(p1) === Person.prototype); // true
console.log(Object.getPrototypeOf(p1).name); // "Nicholas"
let biped = {
numLegs: 2
};
let person = {
name: 'Matt'
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
let biped = {
numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
原型层级
在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应值。
function Person() {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software engineer';
Person.prototype.sayName = function(){
console.log(this.name);
};
let p1 = new Person();
let p2 = new Person();
p1.name = 'Greg';
console.log(p1.name); // 'Greg' 来自实例
console.log(p2.name); // 'Nicholas' 来自原型
function Person() {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software engineer';
Person.prototype.sayName = function(){
console.log(this.name);
};
let p1 = new Person();
let p2 = new Person();
console.log(p1.hasOwnProperty('name')); // false
p1.name = 'Greg';
console.log(p1.hasOwnProperty('name')); // true
delete p1.name;
console.log(p1.hasOwnProperty('name')); // false
原型和in操作符
in可以单独使用或在for-in循环中使用。单独使用时,in会在可以通过对象访问指定属性时返回true,无论该属性在实例上还是原型上。
function Person() {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.sayName = function() {
console.log(this.name);
};
let p1 = new Person();
let p2 = new Person();
console.log(p1.hasOwnProperty('name')); // false
console.log("name" in p1); // true
p1.name = 'Greg';
console.log(p1.name); // 'Greg' 来自实例
console.log(p1.hasOwnProperty('name')); // true
console.log("name" in p1); // true
console.log(p2.name); // 'Nicholas' 来自原型
console.log(p2.hasOwnProperty('name')); // false
console.log('name' in p2); // true
delete p1.name;
console.log(p1.name); // 'Nicholas' 来自原型
console.log(p1.hasOwnProperty('name')); // false
console.log('name' in p1); // true
function hasPrototypeProperty(object, name) {
// 只要通过对象可以访问, in 操作符就返回 true
// hasOwnProperty()只有属性存在于实例上时才返回 true
// 对象拥有该属性但是实例没有,则属性在原型上
return !object.hasOwnProperty('name') && (name in object);
}
function Person() {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software engineer';
Person.prototype.sayName = function() {
console.log(this.name);
};
let keys = Object.keys(Person.prototype);
console.log(keys); // "name,age,job,sayName"
let p1 = new Person();
p1.name = 'Rob';
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name, age]"
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "[constructor,name,age,job,sayName]"
let k1 = Symbol('k1');
let k2 = Symbol('k2');
let o = {
[k1]: 'k1',
[k2]: 'k2'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]
属性枚举顺序
let k1 = Symbol('k1');
let k2 = Symbol('k2');
let o = {
1: 1,
first: 'first',
[k1]: 'k1',
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)]
ECMAScript2017新增两个静态方法,用于将对象内容转换为序列化的可迭代的格式。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', {}]]
// 非字符串会转换为字符串输出
// 两个方法执行对象的浅复制
console.log(Object.values(o)[0] === o.qux); // true
console.log(Object.entries(o)[0][1] === o.qux); // true
// 符号属性会被忽略
const sym = new Symbol();
const o = {
[sym]: 'foo'
};
console.log(Object.values(o)); // []
console.log(Object.entries(o)); // []
通过字面量来重写原型时,Person.prototype被设置为等于一个通过对象字面量创建的新对象,此时Person.prototype的constructor属性就不指向Person了
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
function Person() {}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 29,
job: 'Software engineer',
sayName() {
console.log(this.name);
}
};
// 这种方法恢复constructor属性会创建一个[[Enumerable]]为true的属性
// 原生constructor属性默认是不可枚举的
// 可以改为使用Object.defineProperty()来定义constructor属性
// Object.defineProperty(Person.prototype, 'constructor', {
// enumerable: false,
// value: Person
// });
因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
console.log(typeof Array.prototype.sort); // function
console.log(typeof String.prototype.substring); // function
String.prototype..startsWith = function(str) {
return this.indexOf(str) === 0;
};
let msg = "Hello world";
console.log(msg.startsWith('Hello')); // true
function Person() {}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
friends: ['Shelby', 'Court'],
sayName() {
console.log(this.name);
}
}
let p1 = new Person();
let p2 = new Person();
p1.friends.push('Van');
console.log(p1.friends); // Shelby,Court,Van
console.log(p2.friends); // Shelby,Court,Van
console.log(p1.friends === p2.friends); // true
很多面向对象的语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。实现继承是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
默认情况下,所有引用类型都继承自Object,这也是通过原型链实现的。任何函数的默认原型都是一个Object实例,即这个实例内部指针指向Object.prototype。
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,必须在原型赋值之后在添加到原型上。
function SuperType() {
this.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 SuperType() {
this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
// 通过字面量添加新方法,会使上一行无效
SubType.prototype = {
getSuperValue() {
return this.subproperty;
},
someOtherMethod() {
return false;
}
};
let instance = new SubType();
console.log(instance.getSuperValue()); // Error
原型中包含的应用之会在所有实例间共享,所以属性通常会在构造函数中定义而不是在原型上定义。在使用原型实现继承时,原型实际上会变成另一个类型的实例,即原先的实例属性变为了原型属性。 子类型在实例化时不能给父类型的构造函数传参。事实上,无法在不影响所有对象实例的情况下把参数传进父类的构造函数,再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用。
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
思路:在子类构造函数中调用父类构造函数。函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和call()方法以新创建的对象为上下文执行构造函数。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 继承SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // red, blue, green, black
let instance2 = new SubType();
console.log(instance2.colors); // red, blue, green
相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。
function SuperType(name) {
this.name = name;
}
function SubType() {
// 继承SuperType并传参
SuperType.call(this, 'Nicholas');
// 实例属性
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // Nicholas
console.log(instance.age); // 29
盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。由于存在这些问题,构造函数基本上也不能单独使用。
伪经典继承,综合了原型链和盗用构造函数。使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。既可以把方法定义在原型上以实现重用,又可以让每个实例有自己的属性。
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); // "red, blue, green, black"
instance1.sayName(); // "Nicholas"
instance1.sayAge(); // 29
let instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // "red, blue, green"
instance2.sayName(); // "Greg"
instance2.sayAge(); // 27
// 不自定义类型也可以通过原型实现对象之间的信息共享
function object(o) {
function F() {}
F.prototype = o;
return new F();
}// 本质上,object()是对传入对象执行了一次浅复制
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
let anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
let yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); // Shelby, Court, Van, Rob, Barbie
ES5增加Object.create()方法将原型式继承的概念规范化。第一个参数是作为新对象原型的对象,第二个可选参数是给新对象定义额外属性的对象。
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
let anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); // Shelby, Court, Van, Rob, Barbie
let p3 = Object.create(person, {
name: {
value: 'Mike'
}
});
console.log(p3.name); // Mike
原型式继承适合不需要单独创建构造函数,但仍需要在对象间共享信息的场合。属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
function createAnother(original) {
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log('hi');
};
return clone; // 返回这个对象
}
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。该方法给对象添加函数会导致函数难以重用。
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name); // 第二次调用 SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); // 第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
};
寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
SubType.prototype = prototype; // 赋值对象
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
ES6中引入 class 关键字,是一种语法糖,背后仍是原型和构造函数的概念。
// 类声明
class Person {}
// 类表达式
const Animal = class {};
console.log(FunctionExpression); // undefined
var FunctionExpression = function() {};
console.log(FunctionExpression); // function(){}
console.log(ClassExpression); // undefined
var ClassExpression = class {};
console.log(ClassExpression); // class {}
console.log(FunctionExpression); // FunctionExpression(){}
function FunctionExpression() {}
console.log(FunctionExpression); // FunctionExpression(){}
console.log(ClassExpression); // ReferenceError: ClassExpression is not defined
class ClassExpression {}
console.log(ClassExpression); // class ClassExpression {}
{
function FunctionExpression(){}
class ClassExpression {}
}
console.log(FunctionExpression); // FunctionExpression(){}
console.log(ClassExpression); // ReferenceError: ClassExpression is not defined
类可以包含构造函数、实例方法、获取函数、设置函数和静态类方法,但都不是必需的,空的定义类也有效。建议类名大写。
// 类表达式的名称可选,可以通过name属性获取类表达式的名称字符串
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name);
}
}
let p = new Person();
p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined
constructor关键字用于在类定义块内部创建类的构造函数,它会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数。构造函数的定义不是必需的,不定义相当于将构造函数定义为空函数
class Animal {}
class Person {
constructor() {
console.log('person ctor');
}
}
class Vegetable {
constructor() {
this.color = 'green';
}
}
let a = new Animal; // 类实例化时传入的参数会用作构造函数的,如果不需要参数,类名后括号可选
let p = new Person(); // person ctor
let v = new Vegetable();
console.log(v.color); // orange
class Person {}
// 使用类创建一个新实例
p1.constructor(); // TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor();
class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function
class Person {}
console.log(Person.prototype); // { constructor: f() }
console.log(Person === Person.prototype.constructor); // true
与普通构造函数一样,可以使用 instanceof 操作符检查构造函数原型是否存在于实例的原型链中
class Person {}
let p = new Person();
console.log(p instanceof Person); // true
class Person {}
let p1 = new Person();
console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Person.constructor); // false
let p2 = new Person.constructor();
console.log(p2.constructor === Person); // false
console.log(p2 instanceof Person); // false
console.log(p2 instanceof Person.constructor); // true
let classList = [
class {
constructor(id) {
this.id_ = id;
console.log(`instance ${this.id_}`);
}
}
];
function createInstance(classDefinition, id) {
return new classDefinition(id);
}
let foo = createInstance(classList[0], 3141); // instance 3141
let p = new class Foo {
constructor(x) {
console.log(x);
}
}('bar'); // bar
console.log(p); // Foo{}
每次通过 new 调用类标识符时,都会执行类构造函数。该函数内部,可以为新创建的实例(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();
p1.sayName(); // Jack
p2.sayName(); // Jack
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nickname === p2.nickname); // false
p1.name = p1.nickname[0];
p2.name = p2.nickname[0];
p1.sayName(); // Jake
p2.sayName(); // J-Dog
为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance');
}
// 在类块中定义的所有内容都会定义在类的原型上
locate() {
console.log('prototype');
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
可以把方法定义在类构造函数或者类块中,但不能在类块中给原型添加添加原始值或对象作为成员数据
class Person {
name: 'Jake'
} // Uncaught SyntaxError
类方法等同于对象属性,因此可以使用字符串、Symbol或计算的值作为键
const symbolKey = Symbol("symbolKey");
class Person {
stringKey() {
console.log('invoked stringKey');
}
[symbolKey]() {
console.log('invoked symbolKey');
}
['computed' + 'Key']() {
console.log('invoked computedKey');
}
}
let p = new Person();
p.stringKey(); // invoked stringKey
p[symbolKey](); // invoked symbolKey
p.computedKey(); // invoked computedKey
类定义也支持获取和设置访问器
class Person {
set name(newName) {
this.name_ = newName;
}
get name() {
return this.name_;
}
}
let p = new Person();
p.name = "Jane";
console.log(p.name); // Jane
可以在类上定义静态方法,这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,静态成员每个类上只能有一个。静态类成员在类定义中使用 static 关键字作为前缀,在静态成员中,this引用类自身。
class Person {
constructor() {
// 添加到this的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, { constructor: .. }
Person.locate(); // class, class Person{}
class Person {
constructor(age) {
this.age_ = age;
}
sayAge() {
console.log(this.age_);
}
static create() {
return new Person(Math.floor(Math.random() * 100));
}
}
console.log(new Person()); // Person { age_: xx }
类定义不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
// 在类上定义数据成员
Person.greeting = 'My name is ';
// 在原型上定义数据成员
Person.prototype.name = 'Jake';
let p = new Person();
p.sayName(); // My name is Jake
类定义语法支持在原型和类本身上定义生成器方法
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.entire();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
ES6原生支持了继承机制,背后依旧使用的是原型链
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 {}
派生类可通过super关键字引用他们的原型,该关键字只能在派生类中使用,且仅限于类构造函数、实例方法和静态方法内部。
class Vehicle {
consutructor() {
this.hasEngine = true;
}
}
class Bus extends Vehicle {
consutructor() {
// 不要在super()之前引用this,否则会抛出ReferenceError
super(); // 相当于super.constructor()
console.log(this instanceof Vehicle); // true
console.log(this); // Bus { hasEngine: true }
}
}
new Bus();
class Vehicle {
static identify() {
console.log('vehicle');
}
}
class Bus extends Vehicle {
static identify() {
super.identify();
}
}
Bus.identify(); // vehicle
有时需要定义这样一个类,可供其他类继承,但本身不会被实例化。可以通过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 directly cannot be 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(); // Error: Inheriting class must define foo()
class SuperArray extends Array {
shuffle() {
// 洗牌算法
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
}
}
let a = new SuperArray(1,2,3,4,5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // [1,2,3,4,5]
a.shuffle();
console.log(a); // [5,2,1,3,4]
有些内置类型的方法会返回新实例,默认返回实例的类型与原始实例的类型是一致的。如果想覆盖这个默认行为,则可以覆盖Symbol.species访问器,该访问器决定在创建返回的实例时使用的类
把不同类的行为集中到一个类是一种常见JS模式
// extends 后面可以跟JS表达式,只要可以解析为一个类或者构造函数就行
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 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
很多JS框架(特别的React)已经抛弃混入模式,转向组合模式