上一篇《读 ES6 — 字符串、数值、正则的扩展》 将新增 API
做了一些梳理,隔离了一些复杂而低频的知识点,从有限的篇幅分析,可以窥探到 JavaScript 内部代码组织更趋于合理、而本身 API
则变得更加易用和强大。
本篇继续沿着上篇的分析方向,来整理下 ES6
基本类型中的数组
、对象
新增特性。(函数
稍特别,将单独来梳理)
因为它们在 JavaScript 语言中的地位无比重要,无论是 ES5
还是 ES6
中都应该优先掌握它们。本篇着重梳理 ES6
相对于 ES5
的增量知识,但是有些增量使用起来仍然很费劲,用到一个知识点恐怕先得弄清一大片才行——好比对朋友说了一个谎(大家应该都有这样干过吧......),就得用好几个谎才能圆回来。
比如当你开始用起 Array.from()
你可能会遇到 数组的空位的概念
,然后猛然一惊还有这事,那岂不是我曾经有段程序用错了埋了 bug? 比如函数的尾调用优化,是什么鬼呀?还比如对象 Object
搞了一大堆 getOwnPropertyDescriptor()
、setPrototypeOf()
、getPrototypeOf()
这些看着 prototype
就头疼的方法......
确实,这些东西有点繁琐,不过没关系,如之前的理念我们暂且先把它们隔离起来,关在铁笼子里,等咱先打完小怪,在来收拾它们。(怎么感觉和一款叫 “轩辕剑...” 的游戏剧情有点相似)。另外,就是很多 API
可能初期有点抗拒,只要用起来了就觉得太 TM 顺手丝滑了。
数组的扩展
一如既往,啰嗦完毕开始进入正题。数组新增的特性有:
- 新增方法(包括在命名空间
Array
上和数组实例上) - spread 运算符
- 数组空位的概念
新增方法
数组实例的新增方法:
在数组内自我覆盖: copyWithin()
见好就收: find() 和 findIndex()
是否包含了某项: includes()
自我填充: fill()
花式遍历: entries()、keys() 和 values()
以及命名空间 Array
上的方法:
数组产自近亲: Array.from()
化零为整: Array.of()
新增方法都很简单,和上一篇总结的特征规律是相似的。比如封装高频的写法:
// ES5 数组是否存在某项
var isExist = [1, NaN, 3, 6, 8].indexOf(6) !== -1;
// => 这样写总感觉不够直观
// => 经常还要查 `indexOf` 的 `O` 是大写还是小写有木有!
// => 因为 `typeof` 的是小写!
// 莫名其妙 `NaN` 的存在性判断不出来
[1, NaN, 3, 6, 8].indexOf(NaN);
// => -1
// ES6 数组是否存在某项就没有上述的问题
let isExist = [1, NaN, 3, 6, 8].includes(NaN)
另外比较重要的特征就是:
- 几乎都是纯函数
- 更偏向声明式编程
- API 的行为统一明确
首先,为什么说几乎都是 “纯函数”(相关知识若有需要,可以阅读本人的另一篇专题文章《走向 JavaScript 函数式编程》)? 新增的方法都满足相同输入恒有相同输出,但是还有一点点 “副作用”,就是改变了外部变量即当前数组本身。但总的来说,比起 ES5
,已向 “确定性” API
迈出了一大步。
于是,将稍有 “副作用” 的方法改成 “纯函数”,是非常容易的:
// 带副作用的 fill 方法会改变数组 arr
arr.fill(val, start, end);
// 很容易封装成纯函数,并且实现一定的柯里化
function fill(arr) {
let inner_arr = arr.slice(0);
return function (val, start, end) {
return inner_arr.fill(val, start, end);
}
}
其次,偏向于声明式编程。ES6
看起来是要围剿 for
循环,让它们少出头露面的办法就是提供新的 API
来隐藏它们。
比如,在一次 “寻找 NaN
” 的活动中,这样的调用是不是更 “声明式”,更丝滑顺畅?
//在数组中寻找 NaN
let index = [1, 2, 3, NaN, 6, 8].findIndex(Number.isNaN);
// => 3
let item = [1, 2, 3, NaN, 6, 8].find(Number.isNaN);
// => NaN
至于统一而确定的行为,在上例中 includes
以及 Array.from()
都有体现。对于一个重量级的语言,尤其是欲意 “征服全宇宙” 的 JavaScript 来说,定当以极为挑剔的眼光审视它,而 ES6
已经做了很多。
spread 运算符
spread 运算符(扩展运算符)是三个点(...
)。这个新增项非常能让人接受,而且它已经蔓延到了除数字本身外的 函数参数
(类似数组)、对象
、字符串
等等类型上。
以一个简单的例子,看看用 ...
书写带来的良好的阅读体验:
// 合并数组,通过 concat() 连接起来
var compose = first_arr.concat(second_arr).concat(third_arr);
//毫无杂质的 ...
let compose = [...first_arr, ...second_arr, ...third_arr];
数组空位
最后数组的空位概念,本文不打算去说明。知识点本身比较简单,但是牵扯到太多的验证,比较难梳理,感觉像是挥不走的苍蝇。所以,最好的办法是在编程实践中再去 “拍” 它。
对象的扩展
其实对象的扩展并不复杂,归结起来差不多以下内容:
- 为了更好的
赋值运算
- 属性的简写
- 扩展运算符
- Object 命名空间的新增方法
- 常用方法
- 对象的 prototype 方法
- 属性的描述方法
为了更好的赋值运算
属性的简写、扩展运算符以及与此紧密相关的解构赋值,可以说为旧的 JavaScript 开创了一批新的赋值运算
方式,让 赋值运算
摆脱了一板一眼的 =
号运算的方式。
var lang = {name: 'ECMA2015', shortName: 'ES6'};
//ES5 写法比较冗余
var name = lang.name;
var shortName = lang.shortName;
//ES6 写法更加简明
let {name, shortName} = lang;
对一个 ES6
模块的导入 (import
) 和导出 (export
) ,也能充分凸显这种 赋值运算
的便利性。
// a.js
const version = '1.0.0';
let fn1 = () => {};
let fn2 = () => {};
export { version, fn1, fn2 }; //注:为了节约空间,就写成一行了
//b.js
import { version, fn1 } from './a';
// 如果不能解构赋值
import a from './a';
let version = a.verion;
let fn1 = a.fn1;
属性的简写
上述 赋值运算
,和一个符号有关,那就是 ES6
的大括号{}
,与 ES5
不同的是,ES6
的{}
不仅能开辟一块作用域,又能进行 模式匹配
运算,有如自带魔法一般。
先来看看一个有意思的例子:将任意一个变量变成对象。
// ES5 需要获取形参名
function var2obj(x) {
var obj = {};
var str_fn = var2obk.toString();
var key = str_fn.split(')')[0].split('(')[1];
obj[key] = x;
return obj;
}
var x = 'unkown';
var myObj = var2obj(x);
// => {x: 'unkown'};
更为完整的情形,请参考 这里 。但在 ES6
的 {}
眼里,完全是另一番景象。甚至可以通过这个方式,轻易的获取到所有形参名。(此处不去延伸)
function var2obj (x) {
return {x};
}
var x = 'unkown';
var myObj = var2obj(x);
// => {x: 'unkown'};
let y = 'yes';
var anotherObj = var2obj(y);
// => {y: 'yes'};
大括号 {}
自带运算魔法。解析时,能将 {x}
一分为二,自动展开为 {x: x}
。反之,将对象的键值合成一个变量项,写在 {}
中,就是属性的简写。
// 简写形式
let {name, age} = {name: 'jeremy', age: 18};
// 展开形式
let {name: name, age: age} = {name: 'jeremy', age: 18};
简写对象的解构赋值,可以看做是先转化成上述的 “展开形式” ,然后再开始匹配赋值的。
因此,解构赋值 =
号左边的任何键名,都必须来自 =
号右边对象中的某个元素,否则将无法识别(undefined
)。换句话说,=
号右边对象能够访问到的属性,都是可以被解构和赋值给 =
号左边的,包括它原型链上的属性。
function Corder (name) {
this.name = name;
}
Corder.prototype.age = 18;
let {name, age} = new Corder('jeremy');
// => age 18
扩展运算符
对象也有扩展运算符 (...
),和数组的是类似的,简单理解就是剥离了一层大括号 {}
,将对象键值对直接暴露出来。
扩展运算并不难,但是有一个和解构赋值不同的特征,是它不会将原型链上的属性、方法暴露出来。准确的说,扩展运算符是取出对象自身(不包括其原型链上)的所有可遍历属性。
let {...aCoder} = new Corder('jeremy');
console.log(aCoder);
// => {name: ''jeremy'}
let aCoder = Object.assign({}, new Corder('jeremy'));
// => {name: ''jeremy'}
可见,...
和 Object.assign
都没有将原型链上的 age
取出来。
对了,这里提到了 取出对象自身(不包括其原型链上)的所有可遍历属性
,不禁脑袋中又蹦出一个问题:到底哪些运算或方法,只获取到对象自身的可遍历的属性?又有哪些是可以获取到对象原型链上的属性呢?
面对 ES6
不胜枚举的新增特性,信息量的暴增,往往让人烦躁不堪。情绪性的鄙夷油然而生:看吧,就为了解决些小问题,却弄出这么一大堆东西来,有意思吗!
Object 的新增方法
信息量暴增,确有其事。不过本文意在梳理,解决的问题就是从这些繁杂的信息中,提取容易的、有利的为自己所用,其他晦涩麻烦的暂且一并锁在铁笼子里。
从总体上看,ES6
的新增的特性并非没有瑕疵(笔者实际编程中也曾遇到过,以后的篇幅有机会再提),而且很多粒度很小,辨识起来的确很麻烦。但单单从刚才的提问来说,遍历自身属性还是原型链属性,ES6
并没有给我们制造麻烦。
第一类在 Object
命名空间上新增方法:
- Object.assign(target[, source1, source2, ...])
- Object.keys(obj)
- Object.values(obj)
- Object.entries(obj)
- Object.is(a, b)
- Object.getOwnPropertyDescriptors(obj)
除了 Object.is(a, b)
之外,它们都是处理属性自身的可遍历的属性的方法。Own
是专属自有属性的特定命名,所以带 Own
的 API
只遍历到自有属性就很好识别了,包括 ES5
的 obj.hasOwnProperty()
就是这个规则。此外,前文的 ...
扩展运算符是也归类于此。
第二类在 Object
命名空间上新增方法:
- Object.setPrototypeOf(obj, proto)
- Object.getPrototypeOf(obj)
这两个方法是直接对原型链对象的 set
和 get
,虽不用于遍历,但属于和原型链直接打交道的方法。真正能遍历到原型链的还是 ES5
已有的 for in
循环。
到此,问题就非常明确了,仍然只有极少数的 API
可以遍历或者直接操作原型链,其他绝大多数新增 API
,都只明确的限定在对象自有属性的遍历上。
属性的描述方法
对象的每个属性都有一个描述对象(Descriptor)。属性的描述对象在 ES5
就具备了,但当初很少接触到这个概念。具体上,它包含以下项目:
{
value: 'attr', //属性值
writable: true, // 可写
enumerable: true, //可枚举可遍历
configurable: true //可配置
}
描述对象
之于属性
,好比于原子细分成用若干个质子、中子来描述。描述对象
用来对外界开放处置属性的权限,这在设计健壮的 API
是非常有用的。
为此而新增的方法有:
- Object.getOwnPropertyDescriptors(obj)
- Object.getOwnPropertyDescriptor(obj, key)
上文曾提过,该方法只会遍历到对象自身属性。至于其他方面,延伸暂无必要。
ES6 之于遍历
很多时候,提到获取数组、对象中的元素时候,总会说来个 for
循环,来遍历一下。数组、对象大部分的消费方式就是通过遍历。类似的,字符串也有遍历,甚至 Generator + yield
也是对状态的遍历。看过或用过这么多 ES6
的 API
,不知您是否注意到,作为如此共性的 遍历
这事儿,ES6
其实开放了一个底层概念:Iterator
遍历器。
读过 underscore.js
源码的同学,不难发现它在 遍历
问题上也是采用了统一的 口径
——即用数组的 for i++
方式,对象的遍历 for in
最终也是落脚在数组遍历上。
这两个问题摆在一起,能为我们设计 API
提供怎样的启示呢?信息量略大,容我想好再说,哈哈~
最后, Iterator
遍历器这个偏底层的概念,本文还是老套路,把它先关在笼子里,以后再来拜会。