第 8 章 对象、类与面向对象编程
8.1 理解对象
创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法,如下例所示:
let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {
console.log(this.name);
};
// 字面量写法
let person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
8.1.1 属性的类型
属性分两种:数据属性和访问器属性。
1、数据属性
包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置,数据属性有4个特性描述它们的行为:
[[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
[[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
[[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
[[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。
let person = {
name: "Nicholas"
};
这里,我们创建了一个名为 name 的属性,并给它赋予了一个值"Nicholas"。这意味着[[Value]]特性会被设置为"Nicholas",之后对这个值的任何修改都会保存这个位置。
要修改属性的默认特性,就必须使用 Object.defineProperty()方法。这个方法接收 3 个参数:要给其添加属性的对象、属性的名称和一个描述符对象
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"
以上例子创建了一个名为name的属性并给它赋予了一个只读的值“Nicholas”,这个属性就不能再修改了,严格模式下,修改属性的值会抛出错误。
let person = {};
Object.defineProperty(person, "name", {
//不能从对象上删除
configurable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
delete person.name;
console.log(person.name); // "Nicholas"
2、访问器属性
不包含数据值。他们包含一个获取(getter)函数和一个设置(setter)函数,在读取访问器属性时,会调用获取函数,返回一个有效的值,在写入访问器属性时,会调用设置函数并传入新值,有以下四个特性:
[[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
[[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
[[Get]]:获取函数,在读取属性时调用。默认值为 undefined。
[[Set]]:设置函数,在写入属性时调用。默认值为 undefined。
访问器属性是不能直接定义的,必须使用 Object.defineProperty()。
// 定义一个对象,包含伪私有成员 year_和公共成员 edition
let book = {
year_: 2017,
edition: 1
};
Object.defineProperty(book, "year", {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
});
book.year = 2018;
console.log(book.edition); // 2
8.1.2 定义多个属性
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;
}
},
},
});
8.1.3 读取属性的特性
使用 Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get: function () {
return this.year_;
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let yearDescriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(yearDescriptor.value); // undefined
console.log(yearDescriptor.enumerable); // false
console.log(typeof yearDescriptor.get); // "function"
ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors()静态方法,实际上是在每个自有属性上调用Object.getOwnPropertyDescriptor()并在一个新对象中返回它们。
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017,
},
edition: {
value: 1,
},
year: {
get: function () {
return this.year_;
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
},
},
});
console.log(Object.getOwnPropertyDescriptors(book));
8.1.4 合并对象
Object.assign()方法
let dest, src, result;
/**
* 简单复制
*/
dest = {};
src = {
id: 'src'
};
result = Object.assign(dest, src);
// Object.assign 修改目标对象
// 也会返回修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: src }
console.log(dest); // { id: src }
/**
* 多个源对象
*/
dest = {};
result = Object.assign(dest, {
a: 'foo'
}, {
b: 'bar'
});
console.log(result); // { a: foo, b: bar }
/**
* 获取函数与设置函数
*/
dest = {
set a(val) {
console.log(`Invoked dest setter with param ${val}`);
}
};
src = {
get a() {
console.log('Invoked src getter');
return 'foo';
}
};
Object.assign(dest, src);
// 调用 src 的获取方法
// 调用 dest 的设置方法并传入参数"foo"
// 因为这里的设置函数不执行赋值操作
// 所以实际上并没有把值转移过来
console.log("=============>", dest); // { set a(val) {...} }
Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数。
let dest, src, result;
/**
* 覆盖属性
*/
dest = { id: 'dest' };
result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar' });
// Object.assign 会覆盖重复的属性
console.log(result); // { id: src2, a: foo, b: bar }
// 可以通过目标对象上的设置函数观察到覆盖的过程:
dest = {
set id(x) {
console.log(x);
}
};
Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'third' });
// first
// second
// third
/**
* 对象引用
*/
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true
8.1.5 对象标识及相等判定
在es6之前有些特殊情况是===操作符无法正确判断的
如:
// 这些情况在不同 JavaScript 引擎中表现不同,但仍被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 要确定 NaN 的相等性,必须使用极为讨厌的 isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
为此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
// 正确的 NaN 相等判定
console.log(Object.is(NaN, NaN)); // true
8.1.6 增强的对象语法
1、属性值简写
let name = 'Matt';
let person = {
name: name
};
console.log(person); // { name: 'Matt' }
// 等同于:
let name = 'Matt';
let person = {
name
};
console.log(person); // { name: 'Matt' }
2、可计算属性
可以在对象字面量中完成动态属性复制
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' }
3、简写方法名
let person = {
sayName: function(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
//等同于
let person = {
sayName(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
//简写方法名对获取函数和设置函数也是适用的:
name_: '',
get name() {
return this.name_;
},
set name(name) {
this.name_ = name;
},
sayName() {
console.log(`My name is ${this.name_}`);
}
};
person.name = 'Matt';
person.sayName(); // My name is Matt
//简写方法名与可计算属性键相互兼容:
const methodKey = 'sayName';
let person = {
[methodKey](name) {
console.log(`My name is ${name}`);
}
}
8.1.7 对象解构
// 使用对象解构
let person = {
name: 'Matt',
age: 27
};
//另命名
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
//如果给事先声明的变量赋值,必须包含在一对括号中
let personName, personAge;
let person = {
name: 'Matt',
age: 27
};
({name: personName, age: personAge} = person);
console.log(personName, personAge); // Matt, 27
//直接解构
let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27
//如果引用的属性 不存在,报undefined
console.log(job); // undefined
//也可以在解构赋值的同时定义默认值
let { name, job='Software engineer' } = person;
console.log(job); // Software engineer
解构在内部使用函数 ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据 ToObject()的定义),null和 undefined 不能被解构,否则会抛出错误。
let { length } = 'foobar';
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
1、嵌套解构
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
let personCopy = {};
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person);
// 因为一个对象的引用被赋值给 personCopy,所以修改 person.job 对象的属性也会影响 personCopy
person.job.title = 'Hacker'
console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
//嵌套结构
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
// 声明 title 变量并将 person.job.title 的值赋给它
let { job: { title } } = person;
console.log(title); // Software engineer
在外层属性没有定义的情况下不能使用嵌套解构
let person = {
job: {
title: 'Software engineer'
}
};
let personCopy = {};
// foo 在源对象上是 undefined
({
foo: {
bar: personCopy.bar
}
} = person);
// TypeError: Cannot destructure property 'bar' of 'undefined' or 'null'.
// job 在目标对象上是 undefined
({
job: {
title: personCopy.job.title
}
} = person);
// TypeError: Cannot set property 'title' of undefined
2、部分解构
涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分:
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 对象
8.2 创建对象
8.2.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 person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
8.2.2、 构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
和工厂模式的区别
没有显式地创建对象。
属性和方法直接赋值给了 this。
没有 return。
创建实例,使用了new操作符:
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
1、构造函数也是函数
构造函数与普通函数唯一的区别就是调用方式不同。。任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name);
};
}
// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到 window 对象
window.sayName(); // "Greg"
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
2、构造函数的问题
每次定义函数时,都会初始化一个对象
此对前面的例子而言,person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例。
要解决这个问题,可以把函数定义转移到构造函数外部
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
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
这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。
8.2.4 原型模式
每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型
好处:在原型对象上定义的属性和方法可以被对象实例共享
原来在构造函数中直接赋给对象实例的值,可以直接赋值给他们的原型
function Person() {}
// let Person = function() {}; //函数表达式也可以
Person.prototype.name = "js";
Person.prototype.sayName = function () {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "js"
let person2 = new Person();
person2.sayName(); // "js"
console.log(person1.sayName == person2.sayName); // true
1、理解原型
只要创建一个函数,就会给函数创建一个prototype属性(指向原型对象),所有的原型对象自动获得一个名为constructor 的属性,指回与之关联的构造函数。
Person.prototype.constructor == Person //true
用代码理解实例与构造函数的关系:
function Person() {}
// 声明之后,构造函数就有了一个与之关联的原型对象:
console.log(typeof Person.prototype);
console.log(Person.prototype);
// {
// constructor: f Person(),
// [[Prototype]]: Object
// }
// 如前所述,构造函数有一个 prototype 属性 引用其原型对象,而这个原型对象也有一个 constructor 属性,引用这个构造函数换句话说,两者循环引用:
console.log(Person.prototype.constructor === Person); // true
// 正常的原型链都会终止于 Object 的原型对象 Object 原型的原型是 null
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
console.log(Person.prototype.__proto__);
// {
// constructor: f Object(),
// toString: ...
// hasOwnProperty: ...
// isPrototypeOf: ...
// ...
// }
let person1 = new Person(),
person2 = new Person();
//构造函数、原型对象和实例 是 3 个完全不同的对象:
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
/**
* 实例通过__proto__链接到原型对象,
* 它实际上指向隐藏特性[[Prototype]]
* 构造函数通过 prototype 属性链接到原型对象
* 实例与构造函数没有直接联系,与原型对象有直接联系
*/
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.constructor === Person); // true
// 同一个构造函数创建的两个实例 共享同一个原型对象:
console.log(person1.__proto__ === person2.__proto__); // true
//instanceof 检查实例的原型链中 是否包含指定构造函数的原型:
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
下图展示了各个对象之间的关系:
Person.prototype 指向原型对象,而 Person.prototype.contructor 指回 Person 构造函数。原型对象包含 constructor 属性和其他后来添加的属性。Person 的两个实例 person1 和 person2 都只有一个内部属性指回 Person.prototype,而且两者都与构造函数没有直接联系。另
判断两个对象之间的关系:
//两个例子内部都有链接指向Person.prototype
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
ECMAScript 的 Object 类型有一个方法叫 Object.getPrototypeOf(),返回参数的内部特性
[[Prototype]]的值。
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // "Nicholas"
Object 类型还有一个 setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一
个新值。这样就可以重写一个对象的原型继承关系:
let biped = {
numLegs: 2,
};
let person = {
name: "Matt",
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
Object.setPrototypeOf会严重影响代码性能,可以通过Object.create()来创建一个新对象,并且为其指定原型:
let biped = {
numLegs: 2,
};
let person = Object.create(biped);
person.name = "Matt";
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
2、 原型层级
在通过对象访问属性时,会按照这个属性的名称开始搜索,现在对象实例本身搜索,如果找到了就返回,找不到就在原型对象上搜索。
索然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值,如果在实例上添加了一个与原型对象中同名的属性,那么会在实例上创建这个属性,并且会盖住原型对象上的属性,不会修改它,但是会屏蔽对他的访问。
使用delete可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
console.log(person2.name); // "Nicholas",来自原型
hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
console.log(person1.hasOwnProperty("name")); // false
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
console.log(person1.hasOwnProperty("name")); // true
console.log(person2.name); // "Nicholas",来自原型
console.log(person2.hasOwnProperty("name")); // false
delete person1.name;
console.log(person1.name); // "Nicholas",来自原型
console.log(person1.hasOwnProperty("name")); // false
3、原型和in操作符
单独使用时,in操作符会在可以通过对象访问指定属性时返回true,无论在实例上还是原型上
function Person() {}
Person.prototype.name = "Nicholas";
let person1 = new Person();
let person2 = new Person();
console.log(person1.name); // "Greg",来自原型
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
console.log(person1.hasOwnProperty("name")); // true
console.log("name" in person1); // true
delete person1.name;
console.log(person1.name); // "Nicholas",来自原型
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true
如果要确定某个属性是否存在于原型上,则可以同时使用hasOwnProperty()和in
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
获得对象所有可枚举的实例属性:Object.keys()
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
};
console.log(Object.keys(Person.prototype)); // "name,age,job,sayName"
let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
console.log(Object.keys(p1)); // "[name,age]"
console.log(Object.keys(p1.__proto__)); // " ['name', 'age', 'job', 'sayName']"
如果想列出所有实例属性,无论是否可以枚举,都可以使用 Object.getOwnPropertyNames():
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "[constructor,name,age,job,sayName]"
4、属性枚举顺序
1、不确定:for-in 循环和 Object.keys()
2、确定:Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和 Object.assign()
8.2.5 对象迭代
Object.values()返回对象值的数组;
Object.entries()返回键/值对的数组
注意:非字符串属性会被转换成字符串输出,这2个方法执行对象的浅复制
const sym = Symbol();
const o = {
foo: 'bar',
baz: 1,
qux: {},
[sym]: 'foo' //符号属性会被忽略
};
console.log(Object.values(o)); // ["bar", 1, {}]
console.log(Object.entries((o))); // [["foo", "bar"], ["baz", 1], ["qux", {}]]
1、其他原型语法
字面量写法
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
这样重写之后,Person.prototype 的 constructor 属性就不指向 Person了。在创建函数时,也会创建它的 prototype 对象,同时会自动给这个原型的 constructor 属性赋值。而上面的写法完全重写了默认的 prototype 对象,因此其 constructor 属性也指向了完全不同的新对象(Object 构造函数),不再指向原来的构造函数。虽然 instanceof 操作符还能可靠地返回值,但我们不能再依靠 constructor 属性来识别类型了。
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
如果 constructor 的值很重要,则可以像下面这样在重写原型对象时专门设置一下它的值:
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
但这种方式会创建一个个[[Enumerable]]为 true 的属性。而原生 constructor 属性默认是不可枚举的。因此,如果你使用的是兼容 ECMAScript 的 JavaScript 引擎,那可能会改为使用 Object.defineProperty()方法来定义 constructor 属性:
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
8.3 继承
8.3.1 原型链
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 继承 SuperType
SubType.prototype = new SuperType();
let instance = new SubType();
console.log(instance.getSuperValue()); // true
2、原型与继承关系
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
3、关于方法
子类会覆盖父类的方法:
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
以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。
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()); // 出错!
4、原型链的问题
1、原型中包含的引用值会在所有实例间共享
2、子类型在实例化时不能给父类型的构造函数传参
8.3.2 盗用构造函数
为了解决原型包含引用值导致的继承问题而出现。
思路:在子类构造函数中调用父类构造函数,通过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"
1、传递参数
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
2、盗用构造函数的问题
使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。
8.3.3 组合继承
综合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性
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
8.3.4 原型式继承
创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
8.3.5 寄生式继承
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
8.3.6 寄生式组合继承
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);
};
8.4 类
8.4.1 类定义
// 类声明
class Person {}
// 类表达式
const Animal = class {};
1、与函数定义不同的是,虽然函数声明可以提升,但类定义不能
2、函数受函数作用域限制,而类受块作用域限制
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法.
// 空类定义,有效
class Foo {}
// 有构造函数的类,有效
class Bar {
constructor() {}
}
// 有获取函数的类,有效
class Baz {
get myBaz() {}
}
// 有静态方法的类,有效
class Qux {
static myQux() {}
}
8.4.2 类构造函数
方法名 constructor 会告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数
1、实例化
(1)在内存中创建一个空对象
(2)这个新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性
(3)构造函数内部的this被赋值为这个新对象
(4)执行构造函数内部的代码(给新对象添加属性)
(5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:
class Person {
constructor(name) {
console.log(arguments.length);
this.name = name || null;
}
}
let p1 = new Person; // 0
console.log(p1.name); // null
let p2 = new Person(); // 0
console.log(p2.name); // null
let p3 = new Person('Jake'); // 1
console.log(p3.name); // Jake
8.4.4 继承
1、继承基础:extends
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
2、构造函数、HomeObject 和 super()
派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。
class Vehicle {
constructor() {
this.hasEngine = true;
}
}
class Bus extends Vehicle {
constructor() {
// 不要在调用 super()之前引用 this,否则会抛出 ReferenceError
super(); // 相当于 super.constructor()
console.log(this instanceof Vehicle); // true
console.log(this); // Bus { hasEngine: true }
}
}
new Bus();
在静态方法中可以通过 super 调用继承的类上定义的静态方法:
class Vehicle {
static identify() {
console.log('vehicle');
}
}
class Bus extends Vehicle {
static identify() {
super.identify();
}
}
Bus.identify(); // vehicle
在使用 super 时要注意几个问题。
super 只能在派生类构造函数和静态方法中使用。
class Vehicle {
constructor() {
super();
// SyntaxError: 'super' keyword unexpected
}
}
不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法。
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(super);
// SyntaxError: 'super' keyword unexpected here
}
}
调用 super()会调用父类构造函数,并将返回的实例赋值给 this。
class Vehicle {}
class Bus extends Vehicle {
constructor() {
super();
console.log(this instanceof Vehicle);
}
}
new Bus(); // true
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);
}
}
new Bus();
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor
如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回
一个对象。
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super();
}
}
class Van extends Vehicle {
constructor() {
return {};
}
}
console.log(new Car()); // Car {}
console.log(new Bus()); // Bus {}
console.log(new Van()); // {}