第四章 扩展对象的功能性
1. 对象字面量语法扩展
直接看例子
function createPersonES5(name, age){
return {
name: name,
age: age
}
}
function createPersonES6(name, age){
return {
name,
age
}
}
ES6中通过属性初始化的简写语法,可以消除这种属性名称与局部变量之间的重复书写。当字面量里只有一个属性的名称时,JavaScript引擎会在可访问作用域中查找其同名变量。
2. 对象方法语法扩展
var personES5 = {
name: "haha",
sayName: function () {
console.log(this.name);
}
}
var personES6 = {
name: "haha",
sayName() {
console.log(this.name);
}
}
ES6中消除了冒号和function关键字。在personES6中创建一个sayName()方法,该属性被赋值为一个匿名函数表达式,它拥有在ES5中定义的对象方法所具有的全部特性。二者唯一的区别就是,简写方法可以使用super
关键字。
3. 可计算属性
ES5中想使用变量的值作为对象的属性
var lastName = "last name",
person = {};
person[lastName] = "YouYou";
ES6中可在创建对象字面量的同时使用可计算属性名称。
var lastName = "last name";
person = {
[lastName]: 'HAHAHAHA'
}
4. 新增方法
ES的一个设计目标是不在创建新的全局函数,也不在Object.prototype上创建新的方法。当开发者想向标准添加新方法时,他们会找一个适当的现有对象,让这些方法可用。结果,当没有其他合适的对象时,全局Object对象会收到越来越多的对象方法。
4.1 Object.is()方法
ES6引入Object.is()
方法来弥补全等运算符的不准确运算。
主要处理的是+0和-0; NaN和NaN
console.log(+0 == -0); // true
console.log(+0 === -0); // true
console.log(Object.is(+0, -0)); // false
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(5 == 5); // true
console.log(5 == '5'); // true
console.log(5 === 5); // true
console.log(5 === '5'); // false
console.log(Object.is(5, 5)); // true
console.log(Object.is(5, '5')); // false
4.2 Object.assign()方法
混合方法,和zepto的extend方法完成同样的功能。
接受一个接收对象和任意数量的源对象,最终返回接收对象。
assign使用赋值操作符“=”来赋值相关属性,所以是浅赋值。
var receiver = {};
var object1 = {
array: [1,2,3,4],
name: "HaHa"
}
Object.assign(receiver, object1);
console.log(receiver.array); // [1, 2, 3, 4]
console.log(receiver.name); // HaHa
receiver.array[0] = 123;
console.log(object1.array); // [123, 2, 3, 4]
如果多个源对象具有同名属性,则排位靠后的源对象会覆盖靠前的。
请记住,Object.assign()方法不能将提供者的访问器属性复制到接收对象中。由于Object.assign()方法执行了赋值操作,因此提供者的访问器属性最终会转变为接收对象中的一个数据属性。
var receiver = {},
supplier = {
get name(){
return "HeiHei"
}
}
Object.assign(receiver, supplier);
var descriptor = Object.getOwnPropertyDescriptor(receiver, "name");
console.log(descriptor.value); // HeiHei
console.log(descriptor.get); // undefined
在这段代码中,supplier有一个名为name的访问器属性。当调用Object.assign()方法时返回字符串"HeiHei",因此receiver接收这个字符串后将其存为数据属性receiver.name。
顺便说一下Object.getOwnPropertyDescriptor
MDN
返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
function Sup(name) {
this.name = name;
}
Sup.prototype.sayName = function () {
console.log(this.name);
};
var aa = new Sup(123);
aa.func1 = function(){console.log(123)}
var descriptorSayName = Object.getOwnPropertyDescriptor(aa, 'sayName'); // undefined
var descriptorFunc1 = Object.getOwnPropertyDescriptor(aa, 'func1'); // {value: ƒ(), writable: true, enumerable: true, configurable: true}
再看一个例子:
const source = {
set foo(value) {
console.log(value);
},
sayHi: function(){
console.log('Hi!');
}
};
const target1 = {};
Object.assign(target1, source);
let fooDescriptor = Object.getOwnPropertyDescriptor(target1, 'foo'); // {value: undefined, writable: true, enumerable: true, configurable: true}
let sayHiDescriptor = Object.getOwnPropertyDescriptor(target1, 'sayHi'); // {value: ƒ, writable: true, enumerable: true, configurable: true}
console.log(sayHiDescriptor.value) // function(){console.log('Hi!')};
sayHiDescriptor.value(); // Hi!
上面代码中,source对象的foo属性的值是一个赋值函数,Object.assign方法将这个属性拷贝给target1对象,结果该属性的值变成了undefined。这是因为Object.assign方法总是拷贝一个属性的值(具体的function也是属性的值,录入sayHi方法),而不会拷贝它背后的赋值方法或取值方法。
所以,正确的实现拷贝的方法可以参考:
const shallowMerge = (target, source) => Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
);
利用了defineProperties
和getOwnPropertyDescriptors
。
仍对上例:
let newTarget1 = shallowMerge(target1, source);
Object.getOwnPropertyDescriptor(newTarget1, 'foo')
// {get: undefined, set: ƒ, enumerable: true, configurable: true} TODO: ????
5. 重复的对象字面量
"use strict";
var person = {
name: "Name1",
name: "Name2" // ES5严格模式下会有语法错误
}
console.log(person.name); // Name2
ES6中重复属性检查被移除了,无论在严格还是非严格模式下,代码不再检查重复属性,对于每一组重复属性,都会选取最后一个值。
6. 自有属性枚举顺序
ES5没有规定,由各厂商自行决定。ES6严格定义了对象的自有属性被枚举时的返回顺序。这会影响到Object.getOwnPropertyNames()方法及Reflect.ownKeys方法返回属性的方式,Object.assign()方法处理属性的顺序也将随之改变。
自有属性枚举顺序的基本规则是:
- 所有数字键按升序排序。
- 所有字符串键按照他们被加入对象的顺序排序。
- 所有的symbol键按照它们被加入对象的顺序排序。
var obj = {
a: 'a',
1: 1,
0: 0,
10: 10,
2: 2,
'1': '字符串1',
b: 'b',
d: 'd'
}
obj.c = 1;
console.log(Object.getOwnPropertyNames(obj).join("")); //01210abdc (数字键(包含字符串形式)按升序排序)
console.log(obj[1]); // 字符串1 前面的数字1的值被覆盖
console.log(obj['1']); // 字符串1
对于for-in循环,由于并非所有厂商都遵循相同的实现方式,因此仍未指定一个明确的枚举顺序;而Object.keys()和JSON.stringify()方法都指明与for-in方法使用相同的枚举顺序,因此它们的枚举顺序目前也不明晰。
7. 增强对象的原型
7.1 改变对象的原型
let person = {
getGreeting() {
return "Hello";
}
}
let dog = {
getGreeting() {
return "Woof";
}
};
// 以person对象为原型
let friend = Object.create(person);
console.log(friend.getGreeting()); // Hello
console.log(Object.getPrototypeOf(friend) === person); // true
// 将原型设置为dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting()); // Woof
console.log(Object.getPrototypeOf(friend) === dog); // true
对象原型的真实值被存储在内部专用属性[[Prototype]]中,调用Object.getPrototypeOf会返回其中的值,调用Object.setPrototypeOf会改变其中的值。这不是唯一操作[[Prototype]]的方法。
7.2 简化原型访问的Super调用
如果你想重写对象实例的方法,有需要调用与他同名的原型方法,则在ES5中可以这样实现:
let person = {
getGreeting() {
return "Hello";
}
}
let dog = {
getGreeting() {
return "Woof";
}
};
let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
}
}
// 将原型设置为person
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting()); // Hello, hi!
console.log(Object.getPrototypeOf(friend) === person); // true
对于ES6,重写friend,可以得到相同的结果。
let friend = {
getGreeting() {
// return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
return super.getGreeting() + ", hi!"
}
}
但是换一种写法就会出错,!!必须!!要在使用简写方法的对象中使用Super引用:
let friend = {
// getGreeting() {
// // return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
// return super.getGreeting() + ", hi!"
// }
getGreeting: function(){
return super.getGreeting() + ", hi!" // SyntaxError: 'super' keyword unexpected here
}
}
这个示例中用匿名function定义一个属性,由于在当前上下文中super引用是非法的,因此当调用super.getGreeting时会报错。
Super在多重继承的情况下非常有用,因为在这种情况下,使用Object.getPrototypeOf()方法会出现问题。
let person = {
getGreeting() {
return "Hello";
}
}
let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
// return super.getGreeting() + ", hi!"
}
}
Object.setPrototypeOf(friend, person);
// 原型是friend
let relative = Object.create(friend);
console.log(person.getGreeting()); // Hello
console.log(friend.getGreeting()); // Hello, hi!
console.log(relative.getGreeting()); // Maximum call stack size exceeded
this是relative,relative的原型是friend对象,当执行relative的getGreeting方法时,会调用friend的getGreeting方法,而此时的this的值为relative,所以使用这个方法时Object.getPrototypeOf(this)又找到了friend方法,造成无限死循环下去。
ES5中,这个问题很难解决。ES6中使用super即可解决这个问题。
8. 正式的方法定义(Super寻找的本质)
ES6中正式将方法定义为一个函数,他会有一个内部的[[HomeObject]]属性来容纳这个方法从属的对象。
let person = {
// 是方法
getGreeting(){
return "Hello";
}
}
// 不是方法
function shareGreeting(){
return "hi";
}
Super的所有引用都通过[[HomeObject]]来确定后续的运行过程。
- 在[[HomeObject]]属性上调用Object.getPrototypeOf()方法来检索原型的引用;
- 搜寻原型找到同名函数;
- 设置this绑定并且调用相应的方法
let person = {
getGreeting() {
return "Hello";
}
}
// 以person对象为原型
let friend = {
getGreeting() {
return super.getGreeting() + ", hi!"
}
}
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting()); // Hello, hi!
如果不调用Object.setPrototypeOf(friend, person);方法指定原型,执行到
return super.getGreeting() + ", hi!"
语句时会报错
(intermediate value).getGreeting is not a function
第五章 解构
1. 对象解构
基本语法:
let node = {
type: "Identifier",
name: "foo"
};
let {type, name} = node;
console.log(type); // Identifier
console.log(name); // foo
不要忘记初始化程序 如果使用var、let或const解构声明变量,则必须要提供初始化程序(也就是等号右边的值)
1.1 解构赋值
上一个例子是声明的时候解构,同样可以给变量赋值的时候使用解构语法。
let node = {
type: "Identifier",
name: "foo"
},
type = "Literal",
name = 5;
({ type, name } = node); // 先后顺序无所谓
console.log(type); // Identifier
console.log(name); // foo
注意({ type, name } = node);语句的小括号不能忘,否则会报错
Unexpected token =
JavaScript引擎将一对开放的花括号视为一个代码块,而语法规定,代码块语句不允许出现在赋值语句左侧,添加小括号后可以将块语句转化为一个表达式,从而实现整个解构赋值的过程。
解构表达式的值与表达式右侧(=号左右)的值相等,如此一来,在任何可以使用值的地方都可以使用解构表达式
let node = {
type: "Identifier",
name: "foo"
},
type = "Literal",
name = 5;
function outputInfo(value){
console.log(value === node); // true
}
outputInfo({ type, name } = node);
console.log(type); // Identifier
console.log(name); // foo
解构表达式(也就是=号右侧的表达式)如果为null或undefined会导致程序抛出错误。也就是说,任何尝试读取null或undefined的属性的行为都会触发运行时的错误
TypeError: Cannot match against 'undefined' or 'null'
1.2 默认值
let node = {
type: "Identifier",
name: "foo"
};
let {type, name, value} = node;
console.log(type); // Identifier
console.log(name); // foo
console.log(value); // undefined
使用默认值
let node = {
type: "Identifier",
name: "foo"
};
let {type, name, value = true} = node;
console.log(type); // Identifier
console.log(name); // foo
console.log(value); // true
1.3 为非同名局部变量赋值
let node = {
type: "Identifier",
name: "foo"
};
let {type: localType, name: localName, value: localValue = "123"} = node;
console.log(localType); // Identifier
console.log(localName); // foo
console.log(localValue); // 123
console.log(type); // Uncaught ReferenceError: type is not defined
console.log(name); // 没有执行
type: localType
语法的含义是读取名为type的属性并将其存储在变量localType中。
1.4 嵌套对象解构
let node = {
type: "Identifier",
name: "foo",
loc: {
start: {
line: 1,
column: 1
},
end:{
line:1,
column: 4
}
}
};
let {loc: {start}} = node;
console.log(start.line); // 1
console.log(start.column); // 1
所有冒号前的标识符都代表在对象中的检索为止,其右侧为被赋值的变量名;如果冒号右侧是花括号,则意味着要赋予的最终值嵌套在对象内部更深的层级中。
同样的,也可以使用非同名变量:
let node = {
type: "Identifier",
name: "foo",
loc: {
start: {
line: 1,
column: 1
},
end:{
line:1,
column: 4
}
}
};
let {loc: {start: localStart}} = node;
console.log(localStart.line); // 1
console.log(localStart.column); // 1
语法警示
在使用嵌套解构功能时,你很可能无意中创建了一个无效表达式。
let {loc: {}} = node;
由于右侧只有一对花括号,因而不会声明任何绑定。这个语法在将来可能会被废弃,要警示自己不要写类似的代码。
2. 数组解构
let color = ['red', 'green', 'blue'];
let [firstColor, secondColor] = color;
console.log(firstColor); // red
console.log(secondColor); // green
let [, , thirdColor] = color;
console.log(thirdColor); // blue
解构赋值
let color = ['red', 'green', 'blue'];
let firstColor = 'black';
let secondColor = 'purple';
[firstColor, secondColor] = color;
console.log(firstColor); // red
console.log(secondColor); // green
这与解构对象赋值类似,只不过不在需要小括号包裹。
数组解构还有一个独特的用例:交换两个变量的值
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a); // 2
console.log(b); // 1
嵌套数组解构
let colors = ['red', ['green', 'lightgreen'], 'blue'];
let [firstColor, [secondColor]] = colors;
console.log(firstColor); // red
console.log(secondColor); // green
不定元素
let colors = ['red', 'green', 'lightgreen', 'blue'];
let [firstColor, ...restColor] = colors;
console.log(firstColor); // red
console.log(restColor.length); // 3
console.log(restColor[0]); // green
不定元素可以用来复制数组
let colors = ['red', 'green', 'lightgreen', 'blue'];
let ES5Clone = colors.concat();
let [...ES6cloneColors] = colors;
3. 混合解构
let node = {
type: "Identifier",
name: "foo",
loc: {
start: {
line: 1,
column: 1
}
},
range: [0, 3]
};
let {
loc : {start},
range: [startIndex]
} = node;
console.log(start.line); // 1
console.log(startIndex); // 0
这个特性在从JSON配置中提取信息时极为有效。
4. 解构参数
解构可以用在函数参数的传递过程中,这种使用方式更特别。当定义一个接收大量可选参数的JavaScript函数时,我们通常会创建一个可选对象,将额外的参数定义为这个对象的属性:
// ES5
function setCookie(name, value, options) {
options = options || {};
let secure = options.secure,
path = options.path,
domain = options.domain,
expire = options.expire;
// 设置cookie的代码
}
setCookie('type', 'js', {
secure: true,
expire: 600000
})
这个函数的问题是,只查看函数的声明部分,无法辨识函数的预期参数。
如果将options定义为解构参数,则可以更清晰地了解函数预期传入的参数。
// ES6
function setCookie(name, value, {secure, path, domain, expire}) {
// 设置cookie的代码
}
setCookie('type', 'js', {
secure: true,
expire: 600000
})
这个函数更简单清晰。
但是有一个问题,如果不传第三个参数会报错Cannot match against 'undefined' or 'null'.
这是因为调用setCookie函数时浏览器的实际执行为:
function setCookie(name, value, options) {
let {secure, path, domain, expire} = options;
// 设置cookie的代码
}
如果解构参数是必须的,则可以忽略这个问题。但如果希望将解构参数设为可选的,那么就必须为其提供默认值来结果这个问题:
function setCookie(name, value, {secure, path, domain, expire} = {}) {
// 设置cookie的代码
}
setCookie('type', 'js')
解构参数的默认值
可以为解构参数指定默认值
function setCookie(name, value,
{ secure = true,
path = '/',
domain = 'example.com',
expire = new Date(Date.now() + 360000000)
}) {
// 设置cookie的代码
}
然而这样遇上不带第三个参数的情况又会报错,于是改成下面这样
function setCookie(name, value,
{ secure = true,
path = '/',
domain = 'example.com',
expire = new Date(Date.now() + 360000000)
} = {
secure: true,
path: '/',
domain: 'example.com',
expire: new Date(Date.now() + 360000000)
}
) {
// 设置cookie的代码
}
setCookie('type', 'js')
看起来是不是更复杂了..... 注意第三个参数 前面一部分是 = 号, 后面一部分是 : 号
那好吧,再改。
const setCookieDefaults = {
secure: true,
path: '/',
domain: 'example.com',
expire: new Date(Date.now() + 360000000)
}
function setCookie(name, value,
{ secure = setCookieDefaults.secure,
path = setCookieDefaults.path,
domain = setCookieDefaults.domain,
expire = setCookieDefaults.expire
} = setCookieDefaults
) {
// 设置cookie的代码
}
这下真的没法再改了,这样虽然在全局里多引入了一个变量setCookieDefaults (以前明明可以没有,或者在函数里)。
使用解构参数后,不得不面对处理默认参数的复杂逻辑,但它也有好的一面,如果要改变默认值,可以立即在setCookieDefaults 中修改,数据将自动同步到所有出现过的地方。(强词夺理的感觉)
第六章 Symbol和Symbol属性
在ES5中,语言包含五中原始类型:字符串型、数字型、布尔型、null和undefined。ES6引入了第六种原始类型:Symbol。
1. 创建Symbol
所有的原始值,除了Symbol意外都有各自的字面形式,例如布尔类型的true或数字类型的42。可以通过全局的Symbol函数创建一个Symbol。
let firstName = Symbol();
let person = {};
person[firstName] = "NowhereToRun";
console.log(person[firstName]); // NowhereToRun
上述代码,创建了一个名为firstName的Symbol,用它将一个新的属性赋值给person对象,每当你想访问这个属性时一定要用到最初定义的Symbol。
由于Symbol是原始值,因此调用
new Symbol()
会导致程序抛出错误。//Uncaught TypeError: Symbol is not a constructor
Symbol接受一个可选参数,其可以让你添加一段文本描述即将创建的Symbol,这段描述不可用于属性访问,但是建议背刺黄建Symbol时都添加,以便阅读代码和调试Symbol程序。
let firstName = Symbol("first name");
let person = {};
person[firstName] = "NowhereToRun";
console.log("first name" in person); // false
console.log(person[firstName]); // NowhereToRun
console.log(firstName); // Symbol(first name)
console.log(typeof firstName); // symbol
Symbol的描述被存储在内部的[[Description]]属性中,只有当调用Symbol的toString()
方法时才可以读取这个属性。在执行console.log()时隐式调用了firstname的toString()方法,但是不能直接在代码里访问[[Description]]。
辨识Symbol可以使用typeof操作符。
2. Symbol的使用方法
所有可计算属性名的地方,都可以使用Symbol。
let firstName = Symbol("first name");
// 使用一个可计算对象字面量属性
let person = {
[firstName]: "NowhereToRun"
};
// 将属性设置为只读
Object.defineProperty(person, firstName, {
writable: false
});
let lastName = Symbol("last name");
Object.defineProperties(person,{
[lastName]: {
value: "wagaga",
writable: false
}
});
console.log(person[firstName]); // NowhereToRun
console.log(person[lastName]); // wagaga
3. Symbol共享体系
有时我们可能希望在不同的代码中共享一个Symbol,ES6提供了一个可以随时访问的全局Symbol注册表。
如果想创建一个可共享的Symbol,要使用Symbol.for()
方法,它只接受一个参数,也就是即将创建的Symbol的字符串标识符,这个参数同样也被用作Symbol的描述。
let uid = Symbol.for("uid");
let object = {
[uid]: "12345"
}
console.log(object[uid]); // 12345
console.log(uid); // Symbol(uid)
let uid2 = Symbol.for("uid");
console.log(uid === uid2); // true
console.log(object[uid2]); // 12345
Symbol.for()方法首先在全局Symbol注册表中搜索键为“uid”的Symbol是否存在,如果存在,直接返回已有的Symbol。后续调用会返回相同的Symbol,他俩完全等价。
还有一个与Symbol共享有关的特性:可以使用Symbol.keyFor()方法在Symbol全局注册表中检索与Symbol有关的键。
let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid)); // “uid”
let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2)); // “uid”
let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3)); // undefined
Symbol全局注册表是一个类似全局作用域的共享环境,也就是说你不能假设目前环境中存在哪些建。当使用第三方组件时,尽量使用Symbol键的命名空间以减少命名冲突。例如jQuery的代码可以为所有键添加“jquery”的前缀,“jquery.element”或其他类似的键
4. Symbol属性检索
Object.keys()
和Object.getOwnPropertyNames()
方法可以检索对象中所有的属性名:前一个方法返回所有可枚举的属性名,后一个方法不考虑属性的可枚举性,一律返回。然而为了保持ECMAScript5函数的原有功能,这两个方法都不支持Symbol属性。ES6新增了一个方法Object.getOwnPropertySymbols
方法返回包含所有Symbol自由属性的数组。 如下图