深入浅出ES6
本迷你书是InfoQ特别推出的深入浅出ES6专栏合集迷你书,传送门
ECMAScript 定义了:
- 语言语法 - 语法解析规则、关键字、语句、声明、运算符等。
- 类型:– 布尔型、数字、字符串、对象等。
- 原型和继承
- 内建对象和函数的标准库 – JSON、Math、数组方法、对象自省方法等。
ES5 引入了:
- Object.create()
- Object.defineProperty()
- getters
- setters
- 严格模式
- JSON 对象
迭代器和 for-of 函数
for-of
const arr = [11, 22, 33, 44, 55];
for (var i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
//缺点:内部不能使用break中断循环,也不能使用return语句
arr.forEach((value, index, arr) => {
console.log(value, index, arr);
});
//for-in 是为普通对象设计的,你可以遍历得到字符串类型的键,因此不适用于数组遍历
for (var i in arr) {
console.log(typeof i, i); //i的类型string 0~4
}
//for of 可以正确响应 break、continue 和 return 语句
for (var i of arr) {
console.log(typeof i, i); //i的类型number 11~55
}
优点:
- 这是最简洁、最直接的遍历数组元素的语法
- 这个方法避开了 for-in 循环的所有缺陷
- 与 forEach()不同的是,它可以正确响应 break、continue 和 return 语句
for-of 循环也可以遍历其它的集合 ,如:
- NodeList 对象
- 字符串遍历
- Map 和 Set 对象遍历
let setArr = ["a", "b", "a", "c", "d", "d", "d", "e", "f", "g"];
let mapArr = [["A", "a"], ["B", "b"], ["C", "c"], ["D", "d"]];
let words_set = new Set(setArr);
console.log(words_set); //Set { 'a', 'b', 'c', 'd', 'e', 'f', 'g' }
for (let i of words_set) {
console.log(i);
}
let words_map = new Map(mapArr);
console.log(words_map); // Map { 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd' }
for (let [key, index] of words_map) {
console.log(key, index);
/*
A a
B b
C c
D d
*/
}
for-of 循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用 for-in 循
环(这也是它的本职工作)或内建的 Object.keys()方法:
迭代器对象
你可以给任意类型的对象添加迭代器方法,当你为对象添加 myObject.toString()方法后,就可以将对象转化为字符串,同样地,当你向任意对象添加
myObject[Symbol.iterator]()
方法,就可以遍历这个对象了。所有拥有[Symbol.iterator]()
的对象被称为可迭代的
for-of 循环首先调用集合的[Symbol.iterator]()
方法,紧接着返回一个新的迭代器对
象。迭代器对象可以是任意具有.next()方法的对象;for-of 循环将重复调用这个方法,
每次循环调用一次。举个例子,这段代码是我能想出来的最简单的迭代器:
生成器 Generators
普通函数使用 function 声明,而生成器函数使用 function*声明
在生成器函数内部,有一种类似 return 的语法:关键字 yield,可以 yield 可以只 yield 一次或多次
在生成器的执行过程中,遇到 yield 表达式立即暂停,后续可恢复执行状态。
这就是普通函数和生成器函数之间最大的区别,普通函数不能自暂停,生成器函数
可以。
生成器不是线程
通过实现Symbol.iterator和.next()两个方法你就可以创建自定义迭代器。
模板字符串
-
模板字符串中所有的空格、新行、缩进,都会原样输出在生成的字符串中。
-
反斜杠转义每一个字符
-
它们不能替代模板引擎的地位,例如:Mustache、Nunjucks。
-
标签模板(tagged templates)。
模板字符串由变量和非变量组成,templateData 是一个不可变数组,存储着模板所有的字符串部分(非变量,包括空格),由 JS 引擎为我们创建
//无标签模板字符串简化了简单字符串拼接,标签模板则完全简化了函数调用
function SaferHTML(templateData) {
var s = templateData[0]; // "<p>"
console.log(s, arguments);
for (var i = 1; i < arguments.length; i++) {
var arg = String(arguments[i]); //"哈哈"
// 转义占位符中的特殊字符。
s += arg
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/</g, ">");
// 不转义模板中的特殊字符。
s += templateData[i];
}
return s;
}
var bonk = {
sender: "哈哈",
script: "<script>alert('xss');</script>"
};
var message = SaferHTML`<p>${bonk.sender} 你好</p>`;
// 等价于 var message = SaferHTML(["<p>", "你好.</p>"], bonk.sender);
console.log(message); //<p>哈哈 你好</p>
var dangerMessage = SaferHTML`${bonk.script}`; //代码注入
console.log(dangerMessage); // >script>alert('xss');>/script>
标签模板以开放的姿态欢迎库设计者们来创建强有力的领域特定语言。
不定参数和默认参数
不定参数
函数 containsAll 可以检查一个字符串中是否包含若干个子串,例如:containsAll("banana", "b", "nan")返回 true,containsAll("banana", "c", "nan")返回 false。
function containsAll(haystack) {
console.log(haystack); //banner
console.log(arguments); //["banner", "a", "ban"}
for (var i = 1; i < arguments.length; i++) {
var needle = arguments[i];
if (haystack.indexOf(needle) === -1) {
return false;
}
}
return true;
}
console.log(containsAll("banner", "ac", "ban")); //false
console.log(containsAll("banner", "aer", "ban")); //false
console.log(containsAll("banner", "ann", "ban")); //true
/*
缺点:函数的参数列表中只有一个参数 haystack,我们无法一眼就看出这个函数实际上接
受了多个参数,如果我们想要在 haystack 前后添加另一个参数,必须更新上面代码的for循环体,
*/
不定参数恰好可以解决可读性与参数索引的问题
//不定参数
function containsAll(haystack, ...needles) {
for (var needle of needles) {
if (haystack.indexOf(needle) === -1) {
return false;
}
}
return true;
}
在所有函数参数中,只有最后一个才可以被标记为不定参数,如果没有额外的参数,不定参数就是一个空数组,它永远不会是 undefined
function fn(...args) {
console.log(args);
}
fn(); //[] 空数组
默认参数
function fn(a = 1, b) {
//......
}
//等价
function fn(a = 1, b = undefined) {
//....
}
停止使用 arguments 对象,在使用不定参数或默认参数的函数中禁止使用 arguments 对象
解构 Destructuring
数组解构
var [a,b,c]=[1,2,3];
var [a,[[b],c]]=[1,[[2],3]]
var [,,c]=[1,2,3]
var [a,...b]=[1,2,3] //不定参数
对象解构
- 解构时自定义变量
var person = {
name: "eastboat",
age: 18
};
var { name: myName, age: myAge } = person;
console.log(myName, myAge); //eastboat 18
- 属性名与变量名一致时
var person = {
name: "eastboat",
age: 18
};
var { name, age } = person;
console.log(name, age);
- 嵌套解构
var person02 = {
name: "eastboat",
description: [
"女",
{
hobby: "篮球"
}
]
};
var {
description: [gender, { hobby: hobbies }]
} = person02;
console.log(gender, hobbies); //女 篮球
- 解构未定义属性得到的值是undefined
var { name }={}
console.log(name) //undefined
- 未声明变量时
{name}={name:10}; //报语法错误,js引擎会将任何以{开始的语句解析为一个块语句
//解决方法
({name}={})
重点
当使用对象赋值模式时,被解构的值需要被强制转换为对象。大多数类型都可以被转换为对象,但 null 和 undefined 却无法进行转换。当使用数组赋值模式时,被解构的值一定要包含一个迭代器。
var {name} = null; //TypeError: null has no properties(null 没有属性)
var {name} = NaN; //undefined
默认值
var [missing = true] = [];
var { msg = "Something went wrong" } = {};
var { message: msg = "Something went wrong" } = {};
解构的实际应用
函数参数定义
function say({name,age,gender,addr:address="上海"}){
console.log(`我是${name},性别${gender},今年${age}岁,住在${address}`);
}
var person={
name:'eastboat',
age:18,
gender:"女",
addr:'北京'
}
say(person) //我是eastboat,性别女,今年18岁,住在北京
配置对象参数
ES6 迭代器
var map=new Map();
map.set("name","eastboat");
map.set("age",18);
console.log(map);
for(var [key,value] of map){
console.log(key,value);
}
for(var [key] of map){
console.log(key);
}
for(var [,value] of map){
console.log(value);
}
多重返回值(将结果解构)
function func1() {
return [1, 2]
}
var [a, b] = func1();
console.log(a, b);
function func2() {
var name = 'eastboat';
var age = 18;
return {
c: name,
d: age
}
}
var {
c:name,
d:age
} = func2();
console.log(name, age)
导入部分 CommonJS 模块
const { SourceMapConsumer, SourceNode } = require("source-map");
箭头函数 Arrow Functions
当使用箭头函数创建普通对象时,你总是需要将对象包裹在小括号里
// 为与你玩耍的每一个小狗创建一个新的空对象
var chewToys = puppies.map(puppy => {}); // 这样写会报 Bug!
var chewToys = puppies.map(puppy => ({})); //
在 ES6 中,不需要再 hackthis 了,箭头函数继承了外围作用域的 this 值
Symbols
三种获取 symbol 的方法:
Symbol() //每次调用都会返回一个新的唯一 symbol。
Symbol.for(string) //访问 symbol 注册表,其中存储了已经存在的一系列 symbol
Symbol.iterator
集合
ES6 中的集合本来就是为避免用户数据与内置方法冲突而设计的,所以它们不会把数据作为属性暴露出来
Set
- new Set:创建一个新的、空的 Set。
- new Set(iterable):从任何可遍历数据中提取元素,构造出一个新的集合
- set.size:获取集合的大小,即其中元素的个数
- set.has(value):判定集合中是否含有指定元素,返回一个布尔值。
- set.add(value):添加元素。如果与已有重复,则不产生效果。
- set.delete(value):删除元素。如果并不存在,则不产生效果。.add()和.delete()
都会返回集合自身,所以我们可以用链式语法 - setSymbol.iterator:返回一个新的遍历整个集合的迭代器,一般这个方法
不会被直接调用,因为实际上就是它使集合能够被遍历,也就是说,我们可以
直接写 for (v of set) {...}等等。 - set.forEach(f)
- set.clear():清空集合
- set.keys()、set.values()和 set.entries()返回各种迭代器
Map
- new Map:返回一个新的、空的 Map。
- new Map(pairs):根据所含元素形如[key, value]的数组 pairs 来创建一个新的
Map - map.size:返回 Map 中项目的个数。
- map.has(key):测试一个键名是否存在,类似 key in obj
- map.get(key):返回一个键名对应的值,若键名不存在则返回 undefined,类似
obj[key]。 - map.set(key, value):添加一对新的键值对,如果键名已存在就覆盖
- map.delete(key):按键名删除一项,类似 delete obj[key]
- map.clear():清空 Map
- map.forEach(f)
- map.keys():返回遍历所有键的迭代器。
- map.values():返回遍历所有值的迭代器。
- map.entries():返回遍历所有项的迭代器,就像 mapSymbol.iterator。实
际上,它们就是同一个方法,不同名字。
WeakMap和WeakSet
与 Map、Set 几乎一样的行为,除了以下一些限制:
- WeakMap 只支持 new、has、get、set 和 delete。
- WeakSet 只支持 new、has、add 和 delete。
- WeakSet 的值和 WeakMap 的键必须是对象。
Proxy
现在有一张很好看的风景照片(别人摄影师拍的),还有一张你自己的照片(背景很乱),利用ps我们可以无缝将你的照片作为图片某一部分嵌套进风景照片中,最终合成一张完整的照片,你就可以发表到你的朋友圈了这就是我们所谓的百万P图师的手法(滑稽脸)。
在这过程中,我们会发现颜色,尺寸这些东西是否能够无缝连接两个图片,所谓的颜色,尺寸概念就是指一个 API边界或接口,接口可以详细说明两段代码的交互方式(以什么颜色和尺寸组合起来),以及交互双方对另一半的需求(两张图片颜色是否合适?尺寸是否合适?)。所以如果一旦在我们开发的系统中设计好了接口(颜色尺寸定义好),自然就p图流程(开发流程)就清晰很多了,这样就可以任意替换接口两侧的内容(换成别人的图片,男女老少都可以)而不影响二者的交互过程(以别人的图片作为基础,两张图片也能正好重合在一起)。
如果没有现成的接口(两种图片色差太大,尺寸不一),就需要施展你的创意才华来创造新接口(ps中手动上色,绘图,描边,调整尺寸等等),有史以来最酷的软件 hack 总是会勾勒一些之前从未有过的API边界(解决方案),然后通过大量的工程化实践将接口引入到现有的体系中去。(大家都觉得好,最后就变为通用的p图技巧,就好比向ECMA组织提交新语法草案一样)
想要编写一个 API,你需要充分理解你所面向的对象,就拿上面案例来讲,我们原始的对象就是两张照片,人像和风景图,你在进行p图前就需要分析这个人像和风景图的各自处理的技巧,从色彩学,摄影的拍摄技巧等等方面入手,深入了解图像本身,一旦你理解透彻,就能实现出令人惊异的成果(创造出被大家认可的图片,而且大家都以为这真是你拍的照片~滑稽脸)
-
对象到底是什么? 对象是属性的集合
-
为 JavaScript 对象定义一个 API,一个接口,我们需要什么方法?对象又可以做什么呢?
要编写一个 API,你需要充分理解你所面向的对象.这个问题的答案一定程度上取决于对象的类型: 如:DOM元素对象可以做一部分事情,音频节点对象又可以做另外一部分事情, 但是所有对象都会共享一些基础功能: 1.对象都有属性。你可以 get、set 或删除它们或做更多操作。 2.对象都有原型。这也是 JS 中继承特性的实现方式。 3.有一些对象是可以被调用的函数或构造函数
ECMAScript 标准委员会定义了一个由 14 种内部方法组成的集合,也就是一个适用于所有对象的通用接口,属性、原型和函数这三种基础功能自然成为它们关注的核心。 双方括号[[ ]]代表内部方法,在一般的 JS 代码中不可见,你可以调用、删除或覆写普通方法,但是无法操作内部方法
内部方法 | 说明 | 下列操作会调用内部方法 |
---|---|---|
obj.[[Get]](key, receiver) | 获取属性值 | obj.prop 或 obj[key] |
obj.[[Set]](key, value, receiver) | 为对象的属性赋值。 | obj.prop = value 或 obj[key] = value。 |
obj.[HasProperty] | 检查对象是否存在某属性 | key in obj |
obj.[Enumerate] | 列举对象的可枚举属性 | for (key in obj) |
obj.[GetPrototypeOf] | 返回对象的原型 | obj.[__proto__ ]或Object.getPrototypeOf(obj) |
functionObj.[[Call]](thisValue, arguments) | 调用一个函数 | functionObj()或 x.method() |
constructorObj.[[Construct]](arguments, newTarget) | 调用一个构造函数 | new Date(2890, 6, 2) |
ES6 在对象的中枢系统周围划分了一个清晰的界限,你可以借助代
理特性用任意 JS 代码替换标准中枢系统的内部方法
代理
代理和目标对象之间的关系:
代理的行为很简单:将代理的所有内部方法转发至目标。简单来说,如果调用proxy.[Enumerate],就会返回 target.[Enumerate]。对于所有其它内部方法而言同样可以做到。新创建的代理会尽可能与目标的行为一致
var target = {
//目标对象
}
var handler = {
//句柄对象
}
var proxy = new Proxy(target, handler)
proxy.color = "pink";
console.log(target,handler); // { color: 'pink' } {}
句柄对象
句柄对象的方法可以覆写任意代理的内部方法
var proxy=new Proxy({},{
set:function(t,k,v,receiver){
throw new Error("不要为此对象设置属性")
}
})
proxy.name="Eastboat" //Error: 不要为此对象设置属性
Class定义类
方法定义语法
ES6 提供一种向对象添加特殊属性的新语法.随着 JS 引入越来越多的面向对象方法,人们开始对简化给对象添加访问器的方法感兴趣
function Circle(radius) {
this.radius=radius;
}
//ES5
Circle.prototype = {
area: function area() {
return Math.pow(this.radius, 2) * Math.PI;
}
};
Object.defineProperty(Circle.prototype, "radius", {
get: function() {
return this._radius;
},
set: function(radius) {
if (!Number.isInteger(radius))
throw new Error("圆的半径必须为整数。");
this._radius = radius;
}
});
//ES6
Circle.prototype = {
area() {
return Math.pow(this.radius, 2) * Math.PI;
},
get radius() {
return this._radius;
},
set radius(radius) {
if (!Number.isInteger(radius))
throw new Error("圆的半径必须为整数。");
this._radius = radius;
}
};
我们需要一种功能类似 obj.prop = method 的新方法来给对象添加“方法”,同时不借助 Object.defineProperty 的力量。人们想要能够简单地实现以下功能:
- 给对象添加标准的函数属性
- 给对象添加生成器函数属性。
- 给对象添加标准的访问器函数属性
- 给对象添加任意使用[]语法添加的函数属性,我们称其为预计算(computed)属
性名。
类定义语法
在类的所有实例中,我们只需要一种区分普通函数与特殊函数的方法,关键字static
在一堆方法中指定出唯一的构造函数 关键字 constructor
class Circle{
constructor(radius){
this.radius=radius;
};
static draw(circle,canvas){
//绘制开始
}
}
模块
重命名 import 和 和 export
1 .同时导入两者,我们至少要将其中一个的名称改掉
import {obj as A} from 'one.js'
import {obj as B} from 'two.js'
2.导出的时候也可以重命名
function foo1(){};
function foo2(){};
epport{
foo1 as F1,
foo2 as F2
}
Default exports
import _ from "lodash";
//等价于
import {default as _} from "lodash"
模块对象
当你 import *时,导入的其实是一个模块命名空间对象,模块将它的所有属性都导出了
//COWS.JS
export foo(){}
export fn(){}
// 使用
import * as cows from "cows";
cows.foo()
cows.fn()
聚合模块
有时一个程序包中主模块的代码比较多,为了简化这样的代码,可以用一种统一的方式将其它模块中的内容聚合在一起导出,可以通过这种简单的方式将所有所需内容导入再导出
//导入"index"并将它导出的内容的一部分重新导出
export {component1,component2} from 'index'
//导入"index2"并将它导出的内容全部导出
export * from 'index2'
import 实际都做了些什么
语法解析:阅读模块源代码,检查语法错误
加载:递归地加载所有被导入的模块。这也正是没被标准化的部分。
连接:每遇到一个新加载的模块,为其创建作用域并将模块内声明的所有绑定填充
到该作用域中,其中包括由其它模块导入的内容。
运行时:最终,在每一个新加载的模块体内执行所有语句。此时,导入的过程就已
经结束了,所以当执行到达有一行 import 声明的代码的时候……什么都没发生!
可以将 ES6 系统实现为:在编译时计算所有依赖并将所有
模块打包成一个文件,通过网络一次传输所有模块!像 webpack 这样的工具就实现了
这个功能。