Leetcode icon indicating copy to clipboard operation
Leetcode copied to clipboard

对象、类与面向对象编程

Open webVueBlog opened this issue 2 years ago • 0 comments

  1. 理解对象
  2. 理解对象创建过程
  3. 理解继承
  4. 理解类

理解对象

创建对象:

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);
 }

}; 

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

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

  1. 数据属性

数据属性有 4个特性描述它们的行为。

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true
  • [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true
  • [[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。

要修改属性的默认特性,就必须使用 Object.defineProperty()方法

let person = {};

Object.defineProperty(person, "name", {
 writable: false,
 value: "Nicholas"
});

console.log(person.name); // "Nicholas"
person.name = "Greg";

console.log(person.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"
let person = {};

Object.defineProperty(person, "name", {
 configurable: false,
 value: "Nicholas"
});

// 抛出错误
Object.defineProperty(person, "name", {
 configurable: true,
 value: "Nicholas"
}); 
  1. 访问器属性

它们包含一个获取(getter)函数和一个设置(setter)函数

访问器属性有 4 个特性描述它们的行为。

  • [[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

在不支持 Object.defineProperty()的浏览器中没有办法修改[[Configurable]]或[[Enumerable]]。

定义多个属性

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、writable 和 value 属性。

let book = {};

Object.defineProperties(book, {
 year_: {
 value: 2017
 },
 edition: {
 value: 1
 },
 year: {
 get: function() {
 return this.year_;
 },
 set: function(newValue){
 if (newValue > 2017) {
 this.year_ = newValue;
 this.edition += newValue - 2017;
 }
 }
 }
});

let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"

let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.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));
// {
// 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
// }
// }

合并对象

Object.assign()方法 这个方法接收一个目标对象和一个或多个源对象作为参数 然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回 true)和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象

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

对象标识及相等判定

// 这些是===符合预期的情况
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false

// 这些情况在不同 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

ECMAScript 6 规范新增了 Object.is() 这个方法必须接收两个参数:

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));
}

增强的对象语法

  1. 属性值简写
  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' }

const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key) {
 return `${key}_${uniqueToken++}`;
}
let person = {
 [getUniqueKey(nameKey)]: 'Matt',
 [getUniqueKey(ageKey)]: 27,
 [getUniqueKey(jobKey)]: 'Software engineer'
};
console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
  1. 简写方法名
let person = {
 sayName(name) {
 console.log(`My name is ${name}`);
 }
};
person.sayName('Matt'); // My name is Matt

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

对象解构

// 使用对象解构
let person = {
 name: 'Matt',
 age: 27
};

let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27 

let person = {
 name: 'Matt',
 age: 27
};

let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27 

let person = {
 name: 'Matt',
 age: 27
};

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

let person = {
 name: 'Matt',
 age: 27
};

let { name, job='Software engineer' } = person;
console.log(name); // Matt
console.log(job); // Software engineer

let { length } = 'foobar';
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError 

let personName, personAge;
let person = {
 name: 'Matt',
 age: 27
};
({name: personName, age: personAge} = person);
console.log(personName, personAge); // Matt, 27
  1. 嵌套解构
  2. 部分解构
  3. 参数上下文匹配
  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
  1. 部分解构

需要注意的是,涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分:

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 
  1. 参数上下文匹配
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
printPerson2('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27 

创建对象

工厂模式

用于抽象创建特定对象的过程。

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"); 

这种工厂模式 没有解决对象标识问题(即新创建的对象是什么类型)。

构造函数模式

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

Person()内部 与 工厂函数区别:

  1. 没有显式地创建对象
  2. 属性和方法直接赋值给了 this
  3. 没有 return

创建 Person 的实例

  1. 在内存中创建一个新对象
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

确定对象类型 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

console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true 
  1. 构造函数也是函数

构造函数与普通函数唯一的区别就是调用方式不同。

任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。

// 作为构造函数
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"
  1. 构造函数的问题

构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。

ECMAScript 中的函数是对象 因此每次定义函数时,都会初始化一个对象。

function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = new Function("console.log(this.name)"); // 逻辑等价
} 
console.log(person1.sayName == person2.sayName); // false 

因为都是做一样的事,所以没必要定义两个不同的 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 

导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。

原型模式

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

function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
 console.log(this.name);
};

let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
let Person = function() {};
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
 console.log(this.name);
};

let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
  1. 理解原型

Person.prototype.constructor 指向 Person

在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自Object。

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

Firefox、Safari 和 Chrome会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型

理解原型的行为:

/**
 * 构造函数可以是函数表达式
 * 也可以是函数声明,因此以下两种形式都可以:
 * function Person() {}
 * let Person = function() {}
 */
function Person() {}

/**
 * 声明之后,构造函数就有了一个
 * 与之关联的原型对象:
 */
console.log(typeof Person.prototype);
console.log(Person.prototype);
// {
// constructor: f Person(),
// __proto__: Object
// }

/**
 * 如前所述,构造函数有一个 prototype 属性
 * 引用其原型对象,而这个原型对象也有一个
 * constructor 属性,引用这个构造函数
 * 换句话说,两者循环引用:
 */
console.log(Person.prototype.constructor === Person); // true
/**
 * 如前所述,构造函数有一个 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
conosle.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

本质上,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时返回 true

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

通过原型对象调用 isPrototypeOf()方法检查了 person1 和 person2。

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.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
  1. 原型层级

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

function Person() {}
Person.prototype.name = "Nicholas";

Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
 console.log(this.name);
};

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

console.log(person1.hasOwnProperty("name")); // 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 

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

  1. 原型和 in 操作符

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

确定某个属性是否存在于原型上

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

在 for-in 循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。

要获得对象上所有可枚举的实例属性,可以使用 Object.keys()方法

function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
 console.log(this.name);
};

let keys = Object.keys(Person.prototype);
console.log(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]"

如果想列出所有实例属性,无论是否可以枚举,都可以使用 Object.getOwnPropertyNames():

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

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()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()以及 Object.assign()在属性枚举顺序方面有很大区别

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)] 

对象迭代

Object.values()和 Object.entries()接收一个对象 返回它们内容的数组。

Object.values()返回对象值的数组,Object.entries()返回键/值对的数组。

const o = {
 foo: 'bar',
 baz: 1,
 qux: {}
};
console.log(Object.values(o));

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

符号属性会被忽略:

const sym = Symbol();
const o = {
 [sym]: 'foo'
};

console.log(Object.values(o));
// []
console.log(Object.entries((o)));
// []
  1. 其他原型语法
function Person() {}
Person.prototype = {
 name: "Nicholas",
 age: 29,
 job: "Software Engineer",
 sayName() {
 console.log(this.name);
 }
};

只有一个问题:这样重写之后,Person.prototype 的 constructor 属性就不指向 Person了。

其 constructor 属性也指向了完全不同的新对象(Object 构造函数)

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 属性默认是不可枚举的

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
}); 
  1. 原型的动态性
function Person() {}
let friend = new Person();
Person.prototype = {
 constructor: Person,
 name: "Nicholas",
 age: 29,
 job: "Software Engineer",
 sayName() {
 console.log(this.name);
 }
};
friend.sayName(); // 错误

因为 firend 指向的原型还是最初的原型,而这个原型上并没有 sayName 属性。

  1. 原生对象原型
console.log(typeof Array.prototype.sort); // "function"
console.log(typeof String.prototype.substring); // "function" 
  1. 原型的问题

它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值

function Person() {}
Person.prototype = {
 constructor: Person,
 name: "Nicholas",
 age: 29,
 job: "Software Engineer",
 friends: ["Shelby", "Court"],
sayName() {
 console.log(this.name);
 }
};

let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");

console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true

继承

实现继承是 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

在通过原型链实现继承之后,搜索就可以继承向上,搜索原型的原型

  1. 默认原型

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

Object.prototype

  1. constructor
  2. hasOwnProperty
  3. isPrototypeOf
  4. propertyIsEnumerable
  5. toLocaleString
  6. toString
  7. valueOf

这也是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默认方法的原因。

  1. 原型与继承关系

第一种方式是使用 instanceof 操作符

console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true 

第二种方式是使用 isPrototypeOf()方法

console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true 
  1. 关于方法
function SuperType() {
 this.property = true;
}
SuperType.prototype.getSuperValue = function() {
 return this.property;
};
function SubType() {
 this.subproperty = false;
}

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

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

以对象字面量方式创建原型方法会破坏之前的原型链

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

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

let instance = new SubType();
console.log(instance.getSuperValue()); // 出错!
  1. 原型链的问题

主要问题出现在原型中包含引用值的时候。原型中包含的引用值会在所有实例间共享 ,这也是为什么属性通常会 在构造函数中定义而不会定义在原型上的原因。

原先的实例属性摇身一变成为了原型属性。

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"

这相当于新的 SubType 对象上运行了SuperType()函数中的所有初始化代码。

  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
  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 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

组合继承,是 JavaScript 中使用最多的继承模式。

原型式继承

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"

ECMAScript 5 通过增加 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 person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};

let anotherPerson = Object.create(person, {
 name: {
 value: "Greg"
 }
});
console.log(anotherPerson.name); // "Greg"

寄生式继承

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

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

寄生式组合继承

组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次

一次在是创建子类原型时调用,另一次是在子类构造函数中调用。

本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。

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);
}; 

寄生式组合继承可以算是引用类型继承的最佳模式。

两种主要方式:类声明和类表达式。

// 类声明
class Person {}
// 类表达式
const Animal = class {};
console.log(FunctionExpression); // undefined
var FunctionExpression = function() {};
console.log(FunctionExpression); // function() {}

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

console.log(ClassExpression); // undefined
var ClassExpression = class {};
console.log(ClassExpression); // class {}

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

函数受函数作用域限制,而类受块作用域限制

{
 function FunctionDeclaration() {}
 class ClassDeclaration {}
}

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

类的构成

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,

// 空类定义,有效
class Foo {}

// 有构造函数的类,有效
class Bar {
 constructor() {}
}

// 有获取函数的类,有效
class Baz {
 get myBaz() {}
}

// 有静态方法的类,有效
class Qux {
 static myQux() {}
} 

在把类表达式赋值给变量后,可以通过 name 属性取得类表达式的名称字符串。

let Person = class PersonName {
 identify() {
 console.log(Person.name, PersonName.name);
 }
}

let p = new Person();

p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined

类构造函数

constructor 关键字用于在类定义块内部创建类的构造函数

  1. 实例化

(1) 在内存中创建一个新对象。

(2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。

(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。

(4) 执行构造函数内部的代码(给新对象添加属性)。

(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

class Animal {}
class Person {
 constructor() {
 console.log('person ctor');
 }
}
class Vegetable {
 constructor() {
 this.color = 'orange';
 }
}
let a = new Animal();
let p = new Person(); // person ctor
let v = new Vegetable();
console.log(v.color); // orange

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

class Person {
 constructor(override) {
 this.foo = 'foo';
 if (override) {
 return {
 bar: 'bar'
 };
 }
 }
}
let p1 = new Person(),
 p2 = new Person(true);
console.log(p1); // Person{ foo: 'foo' }
console.log(p1 instanceof Person); // true
console.log(p2); // { bar: 'bar' }
console.log(p2 instanceof Person); // false

调用类构造函数时如果忘了使用 new 则会抛出错误

class Person {}
// 使用类创建一个新实例
let p1 = new Person();
p1.constructor();
// TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor();
  1. 把类当成特殊函数
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 === Person.prototype.constructor); // true

可以使用 instanceof 操作符检查构造函数原型是否存在于实例的原型链中

class Person {}
let p = new Person();
console.log(p instanceof Person); // true 

重点在于,类中定义的 constructor 方法不会被当成构造函数,在对它使用instanceof 操作符时会返回 false

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

类是 JavaScript 的一等公民

// 类可以像函数一样在任何地方定义,比如在数组中
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 {}

实例、原型和类成员

  1. 实例成员

每次通过new调用类标识符时,都会执行类构造函数。

每个实例都对应一个唯一的成员对象

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
  1. 原型方法与访问器

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

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

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

class Person {
 name: 'Jake'
}
// Uncaught SyntaxError: Unexpected token
const symbolKey = Symbol('symbolKey');

class Person {
 stringKey() {
 console.log('invoked stringKey');
 }
 [symbolKey]() {
 console.log('invoked symbolKey');
 }
 ['computed' + 'Key']() {
 console.log('invoked computedKey');
 }
}

let p = new Person();
p.stringKey(); // 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 = 'Jake';
console.log(p.name); // Jake
  1. 静态类方法

在静态成员中,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() {
 // 使用随机年龄创建并返回一个 Person 实例
 return new Person(Math.floor(Math.random()*100));
 }
}
console.log(Person.create()); // Person { age_: ... } 
  1. 非函数原型和类成员
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 
  1. 迭代器与生成器方法
class Person {
 // 在原型上定义生成器方法
 *createNicknameIterator() {
 yield 'Jack';
 yield 'Jake';
 yield 'J-Dog';
 }
 // 在类上定义生成器方法
 static *createJobIterator() {
 yield 'Butcher';
 yield 'Baker';
 yield 'Candlestick maker';
 }
}

let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog

可以通过添加一个默认的迭代器,把类实例变成可迭代对象

class Person {
 constructor() {
 this.nicknames = ['Jack', 'Jake', 'J-Dog'];
 }
 *[Symbol.iterator]() {
 yield *this.nicknames.entries();
 }
}
let p = new Person();
for (let [idx, nickname] of p) {
 console.log(nickname);
} 

也可以只返回迭代器实例:

class Person {
 constructor() {
 this.nicknames = ['Jack', 'Jake', 'J-Dog'];
 }
 [Symbol.iterator]() {
 return this.nicknames.entries();
 }
}
let p = new Person();
for (let [idx, nickname] of p) {
 console.log(nickname);
}
// Jack
// Jake
// J-Dog 

继承

ECMAScript 6 新增特性中最出色的一个就是原生支持了类继承机制。

  1. 继承基础

ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象

class Vehicle {}

// 继承类
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
function Person() {}

// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true
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 {}

注意 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(); // 相当于 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

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

在使用 super 时要注意几个问题。

  1. super 只能在派生类构造函数和静态方法中使用。
class Vehicle {
 constructor() {
 super();
 // SyntaxError: 'super' keyword unexpected
 }
}
  1. 不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法。
class Vehicle {}
class Bus extends Vehicle {
 constructor() {
 console.log(super);
 // SyntaxError: 'super' keyword unexpected here
 }
}
  1. 调用 super()会调用父类构造函数,并将返回的实例赋值给 this。
class Vehicle {}
class Bus extends Vehicle {
 constructor() {
 super();
 console.log(this instanceof Vehicle);
 }
}

new Bus(); // true
  1. super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。
class Vehicle {
 constructor(licensePlate) {
 this.licensePlate = licensePlate;
 }
}

class Bus extends Vehicle {
 constructor(licensePlate) {
 super(licensePlate);
 }
}

console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
  1. 如果没有定义类构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类的参数。
class Vehicle {
 constructor(licensePlate) {
 this.licensePlate = licensePlate;
 }
}

class Bus extends Vehicle {}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
  1. 在类构造函数中,不能在调用 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 
  1. 如果在派生类中显式定义了构造函数,则要么必须在其中调用 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. 抽象基类

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 {
 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); // [3, 1, 4, 5, 2]
class SuperArray extends Array {}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2))
console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // true

如果想覆盖这个默认行为,则可以覆盖 Symbol.species 访问器,这个访问器决定在创建返回的实例时使用的类

class SuperArray extends Array {
 static get [Symbol.species]() {
 return Array;
 }
}

let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2))
console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // false
  1. 类混入

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

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 
  1. 工厂模式就是一个简单的函数,这个函数可以创建对象,为它添加属性和方法,然后返回这个对象。这个模式在构造函数模式出现后就很少用了。
  2. 使用构造函数模式可以自定义引用类型,可以使用 new 关键字像创建内置类型实例一样创建自定义类型的实例。不过,构造函数模式也有不足,主要是其成员无法重用,包括函数。考虑到函数本身是松散的、弱类型的,没有理由让函数不能在多个对象实例间共享。
  3. 原型模式解决了成员共享的问题,只要是添加到构造函数 prototype 上的属性和方法就可以共享。而组合构造函数和原型模式通过构造函数定义实例属性,通过原型定义共享的属性和方法。
  4. 原型式继承可以无须明确定义构造函数而实现继承,本质上是对给定对象执行浅复制。这种操作的结果之后还可以再进一步增强。
  5. 与原型式继承紧密相关的是寄生式继承,即先基于一个对象创建一个新对象,然后再增强这个新对象,最后返回新对象。这个模式也被用在组合继承中,用于避免重复调用父类构造函数导致的浪费。
  6. 寄生组合继承被认为是实现基于类型继承的最有效方式。

🆗

webVueBlog avatar Mar 24 '22 09:03 webVueBlog