# JavaScript 面向对象
# 对象 & 属性
ECMA-262 将对象定义为一组键值对的无序集合。在对象中,键称为属性,每个属性需要映射到一个值,值可以是数据或者函数。
# 属性的类型
ECMA-262 使用一些「 内部特性 」来描述属性的特征。开发者不能在 JavaScript 中直接访问这些特性。
ECMA-262 规范会用两个中括号把特性的名称括起来,来表示它是内部特性,比如 [[Enumerable]]
。
对象中的属性分两种:数据属性 & 访问器属性。
# 数据属性
数据属性包含一个保存数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true
。[[Enumerable]]
:表示能否通过for-in
循环返回 (遍历) 属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true
。[[Writable]]
:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true
。[[Value]]
:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined
。
要修改属性默认的特性,必须使用 Object.defineProperty()
方法. 这个方法接收三个参数:
- 属性所在的对象
- 属性的名字
- 一个描述符对象 descriptor。属性必须是:
configurable
、enumerable
、writable
和value
。设置其中的一或多个值,可以修改对应的特性值。
var person = {};
person.name = "Greg";
alert(person.name); //"Greg"
// name 属性值改成 "Nicholas", 并且设置为不可修改属性值
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas",
});
alert(person.name); //"Nicholas"
person.name = "Greg";
alert(person.name); //"Nicholas"
var person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Nicholas",
});
//抛出错误
Object.defineProperty(person, "name", {
configurable: true,
value: "Nicholas",
});
一旦把属性定义为不可配置 configurable: false
的,就不能再把它变回可配置了。此时,再调用 Object.defineProperty()
方法修改除 writable
之外的特性,都会导致错误.
在调用Object.defineProperty()
方法时,如果不指定,configurable
、enumerable
和 writable
特性的默认值都是 false
。
# 访问器属性
访问器属性不包含数据值;它们包含一对 getter
和 setter
函数(不是必需的)
- 在读取访问器属性时,会调用
getter
函数,这个函数负责返回有效的值; - 在写入访问器属性时,会调用
setter
函数并传入新值,这个函数负责决定如何处理数据。
访问器属性有如下 4 个特性。
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true
。[[Enumerable]]
:表示能否通过for-in
循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true
。[[Get]]
:在读取属性时调用的函数。默认值为undefined
。[[Set]]
:在写入属性时调用的函数。默认值为undefined
。
访问器属性不能直接定义,必须使用 Object.defineProperty()
来定义。
var book = {
// 加 下划线 ("_") 表示只能通过对象方法访问的属性
_year: 2004,
edition: 1,
};
Object.defineProperty(book, "year", {
get: function() {
return this._year;
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
},
});
book.year = 2005;
alert(book.edition); //2
设置访问器属性的值时,允许顺带修改其他属性的值。
只指定 getter
意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了 getter
函数的属性会抛出错误。类似地,只指定 setter
函数的属性也不能读,否则在非严格模式下会返回 undefined
,而在严格模式下会抛出错误。
# Object.defineProperties()
ECMAScript 提供了 Object.defineProperties()
方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:
- 要为之添加或修改属性的对象。
- 一个描述符对象,其属性与要添加或修改的属性一一对应。
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017,
},
edition: {
value: 1,
},
year: {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
},
},
});
# Object.getOwnPropertyDescriptor()
使用Object.getOwnPropertyDescriptor()
方法可以取得指定属性的属性描述符对象。接收两个参数:
- 属性所在的对象。
- 要取得其描述符的属性名。
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor);
// Object { get: get(), set: set(newValue), enumerable: false, configurable: false }
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor);
// Object { value: 2017, writable: false, enumerable: false, configurable: false }
ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors()
静态方法。这个方法实际上会在每个自有属性上调用 Object.defineProperties()
并在一个新对象中返回它们。
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: 2019,
// writable: false
// }
// }
# 合并对象
「 合并 Merge 」的意思是,把源对象所有的本地属性一起复制到目标对象上。有时候这种操作也被称为「 混入 Mixin 」
ECMAScript 6 专门为合并对象提供了 Object.assign()
方法。这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中「 可枚举属性 」和「 自有属性 」复制到目标对象。
- 「 可枚举属性 」
Object.propertyIsEnumerable()
返回true
- 「 自有属性 」
Object.hasOwnProperty()
返回true
let dest = {};
let src = { id: "src" };
let 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 }
/**
* 多个源对象
*/
let dest = {};
let result = Object.assign(dest, { a: "foo" }, { b: "bar" });
console.log(result); // { a: foo, b: bar }
Object.assign()
实际上对每个源对象执行的是浅复制。
并且,如果多个源对象都有相同的属性,则使用最后一个复制的值。
/**
* 浅复制
*/
let dest = {};
let src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true
/**
* 覆盖属性
*/
let dest = { id: "dest" };
let result = Object.assign(
dest,
{ id: "src1", a: "foo" },
{ id: "src2", b: "bar" }
);
// Object.assign会覆盖重复的属性
console.log(result); // { id: src2, a: foo, b: bar }
如果赋值期间出错,则操作会中止并退出,同时抛出错误。Object.assign()
没有 “回滚” 的概念,因此它是一个尽力而为、可能只会完成部分复制的方法。
let dest = {};
let src = {
a: "foo",
get b() {
// Object.assign()在调用这个获取函数时会抛出错误
throw new Error();
},
c: "bar",
};
try {
Object.assign(dest, src);
} catch (e) {}
// Object.assign()没办法回滚已经完成的修改
// 因此在抛出错误之前,目标对象上已经完成的修改会继续存在:
console.log(dest); // { a: foo }
# 增强的对象语法
ECMAScript 6 为定义和操作对象新增了很多极其有用的语法糖特性。
# 属性值简写
当属性名和属性值所用的标识符一样时,就可以省略 :
简写为一个标识符。
var name = "Matt";
var person = {
name: name,
};
// 简写为
var person = {
name,
};
console.log(person); // { 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 引擎运行时将其作为 JavaScript 表达式而不是字符串来求值。
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' }
# 简写方法名
在给对象定义方法时,通常都要写一个方法名、冒号,然后再引用一个匿名函数表达式。
let person = {
sayName: function(name) {
console.log("My name is ${name}");
},
};
现在可以简写为:
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
访问器属性的 Getter 和 Setter 函数也有简写方式:
let person = {
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
# 对象解构语法
「 对象解构 」就是使用与对象匹配的结构来实现对象属性赋值。
使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作。
// 不使用对象解构
var person = {
name: "Matt",
age: 27,
};
var personName = person.name,
personAge = person.age;
// 使用对象解构
var person = {
name: "Matt",
age: 27,
};
var { 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 personName, personAge;
let person = {
name: "Matt",
age: 27,
};
({ name: personName, age: personAge } = person);
// 下面这种写法是错误的:
// { name: personName, age: personAge } = person;
console.log(personName, personAge); // Matt, 27
# 设置默认值
如果引用的属性不存在,则该变量的值就是 undefined
。
也可以在解构赋值的同时定义默认值,这适用于引用的属性不存在于源对象中的情况。
et person = {
name: 'Matt',
age: 27
};
var { name, job } = person;
console.log(name); // Matt
console.log(job); // undefined
// 设置默认值
var { name, job = 'Software engineer' } = person;
console.log(name); // Matt
console.log(job); // Software engineer
# 对基本类型使用解构
当源数据是一个基本类型时,会 JS 引擎会调用 ToObject()
方法 ( 内部方法,运行时环境不可访问 ) 将其转换为对象。
null
和 undefined
不能被解构,否则会抛出错误。
let { length } = "foobar";
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
# 嵌套解构
解构赋值可以使用嵌套结构,以匹配嵌套的属性。
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 = {
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
# 参数上下文匹配
在函数参数列表中进行解构赋值,可以在函数签名中声明在函数体内使用局部变量。
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
# 属性 & 对象所属关系检测
在开发中, 我们需要判断属性与对象的所属关系. 检测方法有如下几种:
# in
运算符
in
操作符, 左边是属性名(字符串), 右边是对象. 如果对象的自有属性或继承属性中包含这个属性, 则返回 true
var obj = {
a: 123,
};
"a" in obj; // true 自有属性
"toString" in obj; // true 继承属性
# hasOwnProperty()
hasOwnProperty()
可以用来判断属性是否为对象自有属性. 参数为属性名的字符串形式.
var obj = {
a: 123,
};
obj.hasOwnProperty("a"); // true
# hasPrototypeProperty()
hasPrototypeProperty()
可以用来判断属性是否为原型对象自有属性.
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person = new Person();
console.log(hasPrototypeProperty(person, "name")); // true
person.name = "Greg";
console.log(hasPrototypeProperty(person, "name")); // false
# 属性枚举
# for/in 循环
for/in 循环用于遍历对象中所有可枚举的属性 ( 不一定是自有的 ). 在循环中, 遍历出的属性名称被赋值给循环变量.
var o = {
a: 1,
b: 2,
c: 3,
};
for (prop in o) {
console.log(prop); // 依次输出 a, b, c
}
# Object.keys()
Object.keys()
返回一个数组, 数组由对象的 "可枚举的自有属性的名称" 组成 (不含 Symbol 属性).
操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用 for...in
循环,而用 Object.keys()
代替。
# Object.getOwnPropertyNames()
Object.getOwnPropertyNames()
返回一个数组, 数组由对象的 "自有属性名称" 组成 (不含 Symbol 属性), 而不仅仅是可枚举的.
# Object.getOwnPropertySymbols()
Object.getOwnPropertySymbols()
返回一个数组,包含对象 "自有" 的所有 Symbol 属性的键名。
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)]
# Reflect.ownKeys()
Reflect.ownKeys()
返回一个数组,包含对象 "自身" 的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
# 属性值枚举
ECMAScript 2017 新增了两个静态方法,用于将对象内容转换为序列化的,可迭代的格式。
Object.values()
返回属性值的数组。Object.entries()
返回键/值对的数组。
符号属性会被忽略。
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", {}]]
另外,这两个方法执行对象的浅复制:
const o = {
qux: {},
};
console.log(Object.values(o)[0] === o.qux);
// true
console.log(Object.entries(o)[0][1] === o.qux);
// true
# 创建对象
# Object 构造函数 & 对象字面量
创建自定义对象,通常方式是创建 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);
},
};
ECMAScript 5.1 并没有正式支持面向对象的结构,比如类或继承。ECMAScript 6 开始正式支持类和继承,但仅仅是封装了 ES5.1 构造函数加原型继承模式的语法糖。
在实际开发时,我们优先使用 ES6 提供的类和继承语法糖。但是对于它的底层概念我们仍旧需要有个了解。
# 工厂模式
工厂模式是一种设计模式,用于抽象创建特定对象的过程。
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
let person1 = createPerson("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() {
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
构造函数 和 工厂模式 相比:
- 没有显式地创建对象;
- 直接将属性和方法赋给了
this
对象; - 没有
return
语句。
用构造函数创建新实例,必须使用 new
操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:
- 创建一个新对象;
- 将新对象作为构造函数的作用域(因此
this
就指向了这个新对象); - 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
用构造函数创建的对象实例, 有一个 constructor
属性, 指向构造函数:
前面的例子里:
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
instanceof
操作符, 可以用来检测对象类型:
例子中创建的所有对象既是 Object
的实例,同时也是 Person
的实例. 所有对象均继承自 Object
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
# 将构造函数当作函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同.
- 只要通过
new
操作符来调用,那它就可以作为构造函数。 - 如果不通过
new
操作符来调用,那它跟普通函数也不会有什么两样。this
指向全局对象window
。
// 当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到 window
window.sayName(); //"Greg"
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
# 构造函数的问题
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
例子中,person1
和 person2
都有一个名为 sayName()
的方法,但那两个方法不是同一个 Function 的实例。 这样不同实例上的同名函数是不相等的.
alert(person1.sayName == person2.sayName); //false
通过把函数定义转移到构造函数外部来解决这个问题:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
但是这样的话, 在外部定义的方法就成了全局函数, 自定义的引用类型丝毫没有封装性. 这些问题可以通过使用原型模式来解决。
# 原型模式
创建的每个函数都有一个 prototype
(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
一般来说开发中我们希望每个实例都有自己的实例属性, 但是共享方法. 所以开发中我们组合使用构造函数模式 & 原型模式。把实例属性放在构造函数, 把共享的属性和方法放在原型对象中.
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
# 理解原型对象
原型对象默认有一个 constructor
(构造函数)属性,这个属性是一个指向原型对象所属构造函数指针.
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针 [[Prototype]]
(内部属性),指向构造函数的原型对象。在很多浏览器上, 对象中的 __proto__
属性指向原型对象。
可以通过 isPrototypeOf()
方法来确定对象之间是否存在原型与实例的关系. 从本质上讲,如果实例对象的 [[Prototype]]
指向调用 isPrototypeOf()
方法的对象,那么这个方法就返回 true
Object.getPrototypeOf()
可以返回 [[Prototype]]
指向的原型对象.
# 对象字面量重写原型对象
可以用一个包含所有属性和方法的对象字面量来重写整个原型对象
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function() {
alert(this.name);
},
};
前面说原型对象的 constructor
属性指向原型对象所在构造函数. 使用对象字面量重写原型对象, constructor
属性也就被改写了.
可以手动设置 constructor
属性的值.
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function() {
alert(this.name);
},
};
注意,以这种方式重设 constructor
属性会导致它的 [[Enumerable]]
特性被设置为 true
。默认情况下,原生的 constructor
属性是不可枚举的
这时可以用 Object.defineProperty()
方法来设置 constructor
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person,
});
# 把原型对象封装在构造函数中
在构造函数外定义原型对象, 可能还是缺乏封装性. 可以改成通过在构造函数中初始化原型. 在初次调用构造函数的时候初始化原型.
function Person(name, age, job) {
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
上面例子中, 只在 sayName()
方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化。
# Object.create()
方法
Object.create() 方法创建一个新对象,使用一个现有的对象来作为新创建的对象的原型 ( __proto__
属性的值 )
const person = {
isHuman: false,
printIntroduction: function() {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
},
};
const me = Object.create(person); // me.__proto__ === person
Object.create()
方法接收两个参数:
proto
必填参数,是新对象的原型对象。如果这个参数是null
,那新对象就彻彻底底是个空对象。propertiesObject
是可选参数,是添加到新对象上的可枚举的属性,及其描述符的对象,这些是新创建对象的自定义的属性和方法,可用hasOwnProperty()
获取的,而不是原型对象上的。
var bb = Object.create(null, {
a: {
value: 2,
writable: true,
configurable: true,
},
});
console.dir(bb); // {a: 2}
console.log(bb.__proto__); // undefined
console.log(bb.__proto__ === Object.prototype); // false
console.log(bb instanceof Object); // false 没有继承`Object.prototype`上的任何属性和方法,所以原型链上不会出现Object
# 继承
简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针 constructor
,而实例都包含一个指向原型对象的内部指针 [[Prototype]]
。
# 原型链
原型链是实现继承的主要方法, 基本思想是让父类型的实例,作为子类型的原型对象,。此时子类型的实例,将共享作为原型的父类型实例的属性和方法,也就实现了对于父类型的继承。
作为子类型的原型对象的父类型实例,将包含一个指向父类型原型的指针。像这样层层递进,就构成了原型的链条。这就是所谓原型链的基本概念。
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;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
这个例子中的实例以及构造函数和原型之间的关系如下图:
# 确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用 instanceof
操作符:
只要操作符前面实例对象的原型链中, 存在操作符后面类型的实例对象, 就返回 true
:
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
第二种方式是使用 isPrototypeOf()
方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此 isPrototypeOf()
方法也会返回 true
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
Object.getPrototypeOf
方法返回指定对象的原型。
Object.getPrototypeOf(instance); // Object { property: true, getSubValue: getSubValue() }
Object.getPrototypeOf(instance) instanceof SuperType; // 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;
};
var instance = new SubType();
alert(instance.getSuperValue()); //false
# 原型链的问题
第一个问题是, 因为原型中的属性会被其实例共享. 在通过原型来实现继承时,实例实际上会变成另一个类型的原型。原先的实例属性也就顺理成章地变成了现在的原型属性了。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
//继承了SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"
第二个问题是, 在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
# 借用构造函数
借用构造函数, 也叫做伪造对象或经典继承, 思想是在子类型构造函数的内部调用超类型构造函数. 通过使用 apply()
和 call()
方法可以在(将来)新创建的对象上执行构造函数.
前面说 this
指向函数调用时所在的执行环境, 或者说调用这个函数的对象. 在创建子类型的实例的时候 call()
方法, 让超类型的 this
指向新实例.
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
//继承了SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"
通过使用 call()
方法(或 apply()
方法也可以),我们实际上是在(未来将要)新创建的 SubType
实例的环境下调用了 SuperType
构造函数。这样一来,就会在新 SubType
对象上执行 SuperType()
函数中定义的所有对象初始化代码。结果, SubType
的每个实例就都会具有自己的 colors
属性的副本了。
# 传递参数
通过借用构造函数, 可以在子类型构造函数中向超类型构造函数传递参数
function SuperType(name) {
this.name = name;
}
function SubType() {
//继承了SuperType,同时还传递了参数
SuperType.call(this, "Nicholas");
//实例属性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29
# 借用构造函数的问题
但是这样的话, 继承来的方法都在构造函数中定义,因此函数复用就无从谈起了。
# 组合继承
组合继承, 也叫做「 伪经典继承 」, 指的是将原型链和借用构造函数的技术组合到一块。
思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
};
function SubType(name, age) {
//继承属性
// 因为在这里传递参数, SubType 原型中的 SuperType 属性会被覆盖
SuperType.call(this, name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
// 因为 SubType 原型是 SuperType 实例, 所以 原型的 construtor 指向的是超类型, 要手动设置.
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
这种是 JavaScript 中最常用的继承模式。
# 类
可以看出,前面所讲的实现继承的代码非常冗长和混乱。
ECMAScript 6 新引入的 class
关键字具有正式定义类的能力。表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
// ES5 原型模式
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return "(" + this.x + ", " + this.y + ")";
};
var p = new Point(1, 2);
上面代码用 class
改写的话为:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return "(" + this.x + ", " + this.y + ")";
}
}
var p = new Point(1, 2);
constructor
方法,这就是构造方法,而 this
关键字则代表实例对象。
定义 “类” 的方法的时候,前面不需要加上 function
这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号 ,
分隔,加了会报错。
创建实例的时候,也是直接对 "类" 使用 new
命令,跟构造函数的用法完全一致。
class Point {
// ...
}
typeof Point; // "function"
Point === Point.prototype.constructor; // true
上面代码表明, 其实 ES6 的类,完全可以看作构造函数的另一种写法。类的数据类型就是函数,类本身就指向构造函数。类的所有方法也都定义在类的 prototype
属性上面。
与 ES5 不同的是, 类的内部所有定义的方法,都是不可枚举的. 这一点与 ES5 的行为不一致。
类不存在函数提升 hoist,这一点与 ES5 完全不同。
new Foo(); // ReferenceError
class Foo {}
# 静态方法 & 静态属性
所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为 “静态方法”。
class Foo {
static classMethod() {
return "hello";
}
}
Foo.classMethod(); // 'hello'
var foo = new Foo();
foo.classMethod();
// TypeError: foo.classMethod is not a function
注意,如果静态方法包含 this
关键字,这个 this
指的是类,而不是实例。
"静态属性" 指的是 Class 本身的属性,而不是定义在实例对象上的属性。
class Foo {}
Foo.prop = 1;
Foo.prop; // 1
上面的写法为 Foo
类定义了一个静态属性 prop
。
现在有一个提案提供了类的静态属性,写法是在实例属性法的前面,加上 static
关键字。
class MyClass {
static myStaticProp = 42;
constructor() {}
}
const x = new MyClass();
console.log(x.myStaticProp); // undefined
console.log(MyClass.myStaticProp); // 42
# extends 继承
Class 可以通过 extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的 constructor(x, y)
this.color = color;
}
toString() {
return this.color + " " + super.toString(); // 调用父类的toString()
}
}
let cp = new ColorPoint(25, 8, "green");
cp instanceof ColorPoint; // true
cp instanceof Point; // true
# super
super
关键字作为函数调用时,表示父类的构造函数,用来新建父类的 this
对象。
子类必须在 constructor
方法中调用 super
方法,否则新建实例时会报错。这是因为子类自己的 this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,在此基础加上子类自己的实例属性和方法。
上面代码中 super()
相当于 Point.prototype.constructor.call(this)
⚠️ 注意: 父类的静态方法,也会被子类继承
# 类的 prototype 属性 & proto 属性
前面说过, ES5 实现之中,每一个实例都有 __proto__
属性,指向对应的构造函数的 prototype
属性。Class 作为构造函数的语法糖,同时有 prototype
属性和 __proto__
属性
- 子类的
__proto__
属性,表示构造函数的继承,总是指向父类。 - 子类的
prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class A {}
class B extends A {}
B.__proto__ === A; // true
B.prototype.__proto__ === A.prototype; // true
可以这样理解:作为一个对象,子类(B)的原型(__proto__
属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype
属性)是父类的原型对象(prototype
属性)的实例。
子类实例的 __proto__
属性的 __proto__
属性,指向父类实例的 __proto__
属性。也就是说,子类的原型的原型,是父类的原型。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, "red");
p2.__proto__ === p1.__proto__; // false
p2.__proto__.__proto__ === p1.__proto__; // true
# 抽象类
虽然 ECMAScript 没有专门支持抽象类,但通过 new.target
也很容易实现。new.target
保存通过 new
关键字调用的类或函数。通过在实例化时检测 new.target
是不是当前类,可以阻止对当前类的实例化:
// 抽象基类
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error("Vehicle cannot be directly instantiated");
}
}
}
// 派生类
class Bus extends Vehicle {}
new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
另外,通过在抽象类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 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()
# 代理 Proxy & 反射 Reflect
ECMAScript 6 新增的代理和反射特性,允许开发者给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用,对于代理对象的操作都会转发到目标对象上。
# 代理基础
最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做。
使用 Proxy
构造函数创的。这个构造函数接收两个参数:目标对象 & 处理程序对象。
要创建空代理,可以传一个简单的对象字面量作为处理程序对象
const target = {
id: "target",
};
const handler = {};
const proxy = new Proxy(target, handler);
// id属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 给目标属性赋值会反映在两个对象上
// 因为两个对象访问的是同一个值
target.id = "foo";
console.log(target.id); // foo
console.log(proxy.id); // foo
// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = "bar";
console.log(target.id); // bar
console.log(proxy.id); // bar
// hasOwnProperty()方法在两个地方
// 都会应用到目标对象
console.log(target.hasOwnProperty("id")); // true
console.log(proxy.hasOwnProperty("id")); // true
// Proxy.prototype是undefined
// 因此不能使用instanceof操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false
# 可撤销代理
使用 new Proxy()
创建的普通代理,代理对象与目标对象之间的联系会在代理对象的生命周期内一直持续存在。
Proxy
提供 revocable()
方法,这个方法支持可撤销代理对象与目标对象的关联。
- 通过调用代理对象的撤销函数
revoke()
就可以断开代理对象与目标对象之间的关系。 - 撤销代理的操作是不可逆的。
const target = {
foo: "bar",
};
const handler = {
get() {
return "intercepted";
},
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke(); // 断开
console.log(proxy.foo); // TypeError
# 代理捕获器 & 反射方法
「 捕获器 Trap 」就是在处理程序对象中定义的 “基本操作的拦截器”。
- 每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作。
- 每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数。
🌰 例如,可以定义一个 get()
捕获器。在代理对象上执行 proxy[property]
、proxy.property
或 Object.create(proxy)[property]
等操作都会触发基本的 get()
操作以获取属性。
注意,只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为。
const target = {
foo: "bar",
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
get() {
return "handler override";
},
};
const proxy = new Proxy(target, handler);
console.log(target.foo); // bar
console.log(proxy.foo); // handler override
console.log(target["foo"]); // bar
console.log(proxy["foo"]); // handler override
console.log(Object.create(target)["foo"]); // bar
console.log(Object.create(proxy)["foo"]); // handler override
# 捕获器参数 & 反射 API
所有捕获器都可以访问相应的参数,比如,get()
捕获器会接收到目标对象、要查询的属性和代理对象三个参数。
const target = {
foo: "bar",
};
const handler = {
get(trapTarget, property, receiver) {
console.log(trapTarget === target);
console.log(property);
console.log(receiver === proxy);
},
};
const proxy = new Proxy(target, handler);
proxy.foo;
// true
// foo
// true
有了这些参数,就可以重建被捕获方法的原始行为:
const target = {
foo: "bar",
};
const handler = {
get(trapTarget, property, receiver) {
return trapTarget[property];
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
但是并不是每个原始行为都如 get()
这么简单,实际上,开发者并不需要手动重建原始行为。
处理程序对象中所有可以捕获的方法都有对应的「 反射 API 方法 」这些方法与捕获器的拦截方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。
因此,使用反射 API 方法就可以重建原始行为。
const target = {
foo: "bar",
};
const handler = {
get() {
return Reflect.get(...arguments);
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
# 捕获器不变式
捕获处理程序的行为必须遵循 “捕获器不变式”。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。
🌰 比如,如果目标对象有一个不可配置且不可写的数据属性,那么在 get()
捕获器返回一个与该属性不同的值时,会抛出 TypeError
:
const target = {};
Object.defineProperty(target, "foo", {
configurable: false,
writable: false,
value: "bar",
});
const handler = {
get() {
return "qux";
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo);
// TypeError
# 捕获器种类
代理可以捕获 13 种不同的基本操作。这些操作有各自不同的反射 API 方法、参数、关联 ECMAScript 操作和不变式。
有几种不同的 JavaScript 操作会调用同一个捕获器处理程序。
get()
捕获器会在获取属性值的操作中被调用。
- 反射 API:
Reflect.get()
- 返回值:返回值无限制
- 拦截的操作:
proxy.property
proxy[property]
Object.create(proxy)[property]
Reflect.get(proxy, property, receiver)
- 捕获器处理程序参数:
target
:目标对象。property
:引用的目标对象上的字符串键属性。receiver
:代理对象或继承代理对象的对象。
- 捕获器不变式:
- 如果
target.property
不可写且不可配置,则处理程序返回的值必须与target.property
匹配。 - 如果
target.property
不可配置且[[Get]]
特性为undefined
,处理程序的返回值也必须是undefined
。
- 如果
const myTarget = {};
const proxy = new Proxy(myTarget, {
get(target, property, receiver) {
console.log("get()");
return Reflect.get(...arguments);
},
});
proxy.foo;
// get()
set()
捕获器会在设置属性值的操作中被调用。
- 反射 API:
Reflect.set()
- 返回值:返回
true
表示成功;返回false
表示失败,严格模式下会抛出 TypeError。 - 拦截的操作:
proxy.property = value
proxy[property] = value
Object.create(proxy)[property] = value
Reflect.set(proxy, property, value, receiver)
- 捕获器处理程序参数:
target
:目标对象。property
:引用的目标对象上的字符串键属性。value
:要赋给属性的值。receiver
:接收最初赋值的对象。
- 捕获器不变式
- 如果
target.property
不可写且不可配置,则不能修改目标属性的值。 - 如果
target.property
不可配置且[[Set]]
特性为undefined
,则不能修改目标属性的值。 - 在严格模式下,处理程序中返回
false
会抛出TypeError
。
- 如果
const myTarget = {};
const proxy = new Proxy(myTarget, {
set(target, property, value, receiver) {
console.log("set()");
return Reflect.set(...arguments);
},
});
proxy.foo = "bar";
// set()
has()
捕获器会在 in
操作符中被调用。
- 反射 API:
Reflect.has()
- 返回值:必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。
- 拦截的操作:
property in proxy
property in Object.create(proxy)
with(proxy) {(property);}
Reflect.has(proxy, property)
- 捕获器处理程序参数:
target
:目标对象。property
:引用的目标对象上的字符串键属性。
- 捕获器不变式
- 如果
target.property
存在且不可配置,则处理程序必须返回true
。 - 如果
target.property
存在且目标对象不可扩展,则处理程序必须返回true
。
- 如果
const myTarget = {};
const proxy = new Proxy(myTarget, {
has(target, property) {
console.log("has()");
return Reflect.has(...arguments);
},
});
"foo" in proxy;
// has()
defineProperty()
捕获器会在 Object.defineProperty()
中被调用。
- 反射 API:
Reflect.defineProperty()
- 返回值:
defineProperty()
必须返回布尔值,表示属性是否成功定义。返回非布尔值会被转型为布尔值。 - 拦截的操作:
Object.defineProperty(proxy, property, descriptor)
Reflect.defineProperty(proxy, property, descriptor)
- 捕获器处理程序参数:
target
:目标对象。property
:引用的目标对象上的字符串键属性。descriptor
:包含可选的enumerable
、configurable
、writable
、value
、get
和set
定义的对象。
- 捕获器不变式:
- 如果目标对象不可扩展,则无法定义属性。
- 如果目标对象有一个可配置的属性,则不能添加同名的不可配置属性。
- 如果目标对象有一个不可配置的属性,则不能添加同名的可配置属性。
const myTarget = {};
const proxy = new Proxy(myTarget, {
defineProperty(target, property, descriptor) {
console.log("defineProperty()");
return Reflect.defineProperty(...arguments);
},
});
Object.defineProperty(proxy, "foo", { value: "bar" });
// defineProperty()
getOwnPropertyDescriptor()
捕获器会在 Object.getOwnPropertyDescriptor()
中被调用。
- 反射 API:
Reflect.getOwnPropertyDescriptor()
- 返回值:
getOwnPropertyDescriptor()
必须返回对象,或者在属性不存在时返回undefined
。 - 拦截的操作:
Object.getOwnPropertyDescriptor(proxy, property)
Reflect.getOwnPropertyDescriptor(proxy, property)
- 捕获器处理程序参数:
target
:目标对象。property
:引用的目标对象上的字符串键属性。
- 捕获器不变式:
- 如果自有的
target.property
存在且不可配置,则处理程序必须返回一个表示该属性存在的对象。 - 如果自有的
target.property
存在且可配置,则处理程序必须返回表示该属性可配置的对象。 - 如果自有的
target.property
存在且target
不可扩展,则处理程序必须返回一个表示该属性存在的对象。 - 如果
target.property
不存在且target
不可扩展,则处理程序必须返回undefined
表示该属性不存在。 - 如果
target.property
不存在,则处理程序不能返回表示该属性可配置的对象。
- 如果自有的
const myTarget = {};
const proxy = new Proxy(myTarget, {
getOwnPropertyDescriptor(target, property) {
console.log("getOwnPropertyDescriptor()");
return Reflect.getOwnPropertyDescriptor(...arguments);
},
});
Object.getOwnPropertyDescriptor(proxy, "foo");
// getOwnPropertyDescriptor()
deleteProperty()
ownKeys()
getPrototypeOf()
setPrototypeOf()
isExtensible()
preventExtensions()
apply()
捕获器会在调用函数时中被调用。
- 反射 API 方法:
Reflect.apply()
- 返回值:返回值无限制。
- 拦截的操作
proxy(...argumentsList)
Function.prototype.apply(thisArg, argumentsList)
Function.prototype.call(thisArg, ...argumentsList)
Reflect.apply(target, thisArgument, argumentsList)
- 捕获器处理程序参数:
target
:目标对象。thisArg
:调用函数时的this
参数。argumentsList
:调用函数时的参数列表。
- 捕获器不变式:
target
必须是一个函数对象。
const myTarget = () => {};
const proxy = new Proxy(myTarget, {
apply(target, thisArg, ...argumentsList) {
console.log("apply()");
return Reflect.apply(...arguments);
},
});
proxy();
// apply()
construct()
捕获器会在 new
操作符中被调用。
- 反射 API:
Reflect.construct()
- 返回值:
construct()
必须返回一个对象。 - 拦截的操作:
new proxy(...argumentsList)
Reflect.construct(target, argumentsList, newTarget)
- 捕获器处理程序参数:
target
:目标构造函数。argumentsList
:传给目标构造函数的参数列表。newTarget
:最初被调用的构造函数。
- 捕获器不变式:
target
必须可以用作构造函数。
const myTarget = function() {};
const proxy = new Proxy(myTarget, {
construct(target, argumentsList, newTarget) {
console.log("construct()");
return Reflect.construct(...arguments);
},
});
new proxy();
// construct()
# 代理模式
使用代理可以在代码中实现一些有用的编程模式。
# 跟踪属性访问
通过捕获 get
、set
和 has
等操作,可以知道对象属性什么时候被访问、被查询。
const user = {
name: "Jake",
};
const proxy = new Proxy(user, {
get(target, property, receiver) {
console.log("Getting ${property}");
return Reflect.get(...arguments);
},
set(target, property, value, receiver) {
console.log("Setting ${property}=${value}");
return Reflect.set(...arguments);
},
});
proxy.name; // Getting name
proxy.age = 27; // Setting age=27
# 隐藏属性
const hiddenProperties = ["foo", "bar"];
const targetObject = {
foo: 1,
bar: 2,
baz: 3,
};
const proxy = new Proxy(targetObject, {
get(target, property) {
if (hiddenProperties.includes(property)) {
return undefined;
} else {
return Reflect.get(...arguments);
}
},
has(target, property) {
if (hiddenProperties.includes(property)) {
return false;
} else {
return Reflect.has(...arguments);
}
},
});
// get()
console.log(proxy.foo); // undefined
console.log(proxy.bar); // undefined
console.log(proxy.baz); // 3
// has()
console.log("foo" in proxy); // false
console.log("bar" in proxy); // false
console.log("baz" in proxy); // true
# 属性验证
因为所有赋值操作都会触发 set()
捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值。
const target = {
onlyNumbersGoHere: 0,
};
const proxy = new Proxy(target, {
set(target, property, value) {
if (typeof value !== "Number") {
return false;
} else {
return Reflect.set(...arguments);
}
},
});
proxy.onlyNumbersGoHere = 1;
console.log(proxy.onlyNumbersGoHere); // 1
proxy.onlyNumbersGoHere = "2";
console.log(proxy.onlyNumbersGoHere); // 1
# 函数参数验证
可以让函数只接收某种类型的值,或者编写其他验证逻辑。
function median(...nums) {
return nums.sort()[Math.floor(nums.length / 2)];
}
const proxy = new Proxy(median, {
apply(target, thisArg, ...argumentsList) {
for (const arg of argumentsList) {
if (typeof arg !== "number") {
throw "Non-number argument provided";
}
}
return Reflect.apply(...arguments);
},
});
console.log(proxy(4, 7, 1)); // 4
console.log(proxy(4, "7", 1));
// Error: Non-number argument provided
类似地,可以要求实例化时必须给构造函数传参:
class User {
constructor(id) {
this.id_ = id;
}
}
const proxy = new Proxy(User, {
construct(target, argumentsList, newTarget) {
if (argumentsList[0] === undefined) {
throw "User cannot be instantiated without id";
} else {
return Reflect.construct(...arguments);
}
},
});
new proxy(1);
new proxy();
// Error: User cannot be instantiated without id
# 数据绑定 & 可观察对象
可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:
const userList = [];
class User {
constructor(name) {
this.name_ = name;
}
}
const proxy = new Proxy(User, {
construct() {
const newUser = Reflect.construct(...arguments);
userList.push(newUser);
return newUser;
},
});
new proxy("John");
new proxy("Jacob");
new proxy("Jingleheimerschmidt");
console.log(userList); // [User {}, User {}, User{}]
还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息:
const userList = [];
function emit(newValue) {
console.log(newValue);
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver) {
const result = Reflect.set(...arguments);
if (result) {
emit(Reflect.get(target, property, receiver));
}
return result;
},
});
proxy.push("John");
// John
proxy.push("Jacob");
// Jacob