一 背景
JavaScript经过二十来年年的发展,由最初简单的交互脚本语言,发展到今天的富客户端交互,后端服务器处理,跨平台(Native),以及小程序等等的应用。JS的角色越来越重要,处理场景越来越复杂。在这个背景下,JS最初的简陋设计显然是不太够用的,其松散的语法规则,拗口的继承机制(传说中的6种继承方法),无命名空间,模块化,以及异步处理的回调地狱等等特性在开发过程中容易成为开发人员的各种痛点,各个JS框架比如jQuery,SeaJs,等等为了这些问题也是操碎了心。不过随着JS语言的发展,上面的各种问题也逐渐得到了优化。
然而JS又是一门神奇的语言,他的大部分场景寄存于浏览器的,不像PHP,ASP.NET,Java背后有一个专一的厂商支持,而是面对IE,Chrome,Firefox,Safari,Opera等各种互为竞争对手厂商,面对的情况略微复杂,其兼容性又可以写一本血泪史。所以本文提到的各种特性在使用之前需要考究下你的用户群体是否支持,降级处理,polyfill转义等等。
关于ES6的书现在已经有很多了,其中包括很多名著,比如阮一峰的ECMAScript 6 入门,本文从我的角度重新梳理了一下,力求由浅入深,并且重点介绍一些比较实用的特性,让知识点更加系统化,清晰化。
二 ES6带来了什么
因为任何一门语言都是在不断的发展和进化的,比如C#语言依托.NetFramework由1.0发展到4.5语言能力不断在增强,PHP由最初的1.0发展到7.0版本,同样JS同样也是在不断的完善进步中的,使用ES6可以给你带来以下变化:
(1) 语法更加规范,语义化更强,不容易出错:引入块级作用域避免变量冲突,降低出错几率。
(2) 语法更加简洁,便于维护:函数默认参数,箭头函数,解构赋值,扩展运算符,字符串变量等等让之前很多冗余代码瞬间消失,可读性更强;类的定义和继承更接近常见的C++和PHP语法,更加便于维护。
(3) 功能更加强大:新增了Map,Set,ArrayBuffer等数据结构,新增了Proxy,Reflect,Decorator等辅助函数,处理对象更加灵活,扩展了JS语言能力。
(4) 异步编程更加直观:摆脱回调地狱,以类似于同步的语法进行异步编程。
(5) 模块化开发:JS终于内置支持了模块,命名空间,变量冲突的问题得到了原生支持。
二 总览
本系列主要从复杂性和实用性两个维度来逐步探讨,主要包括:
(1) JS语言的增强:包括语法的规范,数据类型的扩展,新增数据结构,解构赋值等
(2) 函数的增强:包括针对编写函数的优化语法,默认参数,扩展运算符,箭头函数等
(3) 对象的增强:包括对对象定义和继承的优化写法等
(4) 异步编程:包括Promise,Generator,Async,Await等异步编程写法
(5) 辅助模块:包括Symbol,Proxy,Reflect,Decorator等辅助API,这块因为应用不多,实用性略低一些,所以本文暂不讨论,如果有兴趣可以查询相关资料。
(6) 模块化编程:包括针对JS模块的语言层级的支持
三 本篇目录
本篇作为JS语言的增强篇,主要有以下内容:
(1) let/const/块级作用域
(2) 解构赋值
(3) 字符串/数组的扩展
(4) Set/Map数据结构
(5) ArrayBuffer数据结构
四 开始
1. let/const/块级作用域:
我们知道JS语言var是来定义变量的,而且是动态变量非常方便,ES6为何要发明let和const?
ES6之前没有块级作用域的说法,而只有函数作用域,这是JS跟C++,PHP相比一个很明显的特性:
void main()
{
int num = ;
int index = ;
if(index > )
{
int test = ;
}
printf("%d/n",test);
}
运行时会test变量不存在,因为test变量只能在if的花括号内有效,出了花括号会销毁,但是JS就不一样了:
function main()
{
var num = 2;
var index = 1;
if(index > 0){
var test = 10;
}
document.write(test);
}
妥妥的没问题,因为定义的test变量在main函数内都是有效的,为什么呢,因为在JS里面存在变量提升和函数声明提升的过程,最终执行结果其实是这样子:
function main()
{
var num,index,test;
num = 2;
index = 1;
if(index > 0){
test = 10;
}
document.write(test);
}
所以在ES6之前,var声明变量就有这些特性,变量提升,函数作用域,以及可以重复命名,每次命名都是一次覆盖,所以使用var变量容易出现坑:
(1) 因为变量可以提升,变量可以在任何位置声明,回溯性不好,维护性不好。
(2) 允许同名,内部变量覆盖外部变量 ,下面的例子本意是想先用外部变量,但实际上被内部变量覆盖了。
var name = "michael";
function test(){
console.log(name);
var name = "leo";
}
test();//undefined
(3) for循环变量容易泄露为全局变量,而且在数组赋值中会引起歧义
var func = [];
for (var index = 0; index < 10; index++) {
func[index] = function(){
console.log(index);
};
}
console.log(index); //10
func[0](); //10
上面的例子存在两个问题:index变量在循环结束后暴露在全局污染外部函数,func数组的每个成员打印出来的都是10。因为他们引用的是同一个全局变量index,而这个变量在调用func[0]的时候值是10,所以就需要使用闭包来创建变量的方式来解决。
而let和const声明变量就可以避免以上问题
(1) 不存在变量提升,变量必须先声明再使用,更加规范严谨
(2) 变量名不能重复定义,避免歧义覆盖
(3) 定义的变量引入块级作用域,只有块级作用域内有效,避免内外部变量相互污染,再也不用使用匿名函数了。
(4) for循环的计算器内部使用,index用完即销毁,而且不再需要闭包来处理,因为let在每次循环都重新生成,而且会自动记录上次的值,这是ES6在处理循环的一个特性。
let func = [];
for (let index = 0; index < 10; index++) {
func[index] = function(){
console.log(index);
};
}
func[0](); //
console.log(index); //index is not defined
let和const的区别
let 用于可变值的变量,一旦声明作用域就会被限定在本块。
const用于定义常亮,比如PI等等,对于简单类型的数据(数值、字符串、布尔值),const定义以后地址就会固定,所以值也不能改变,但是对于引用类型(数组,对象),虽然地址是不会变的,但是const对于地址指向的内存堆的值是可以变的,也就是说const声明的对象属性是可以修改的,这一点需要留意。
2. 解构赋值:
解构赋值是访问数组和对象的一种便捷方式,可以让代码更加简洁优雅。比如访问数组,以前我们用的方式是:
let name = "mic",
sex = "male",
city = "深圳",
height = 1.65;
用解构的方式,明显代码简洁了很多。
let [name,sex,city,height] = ["mic","male","深圳",1.65];
(1) 数组解构赋值
数组的结构赋值就是把右边数组按位置赋值到左边的变量,可以嵌套,可以为空。
let [name, [[city], sex]] = ["mic", [["深圳"], "male"]];
let [ , , name] = ["深圳", "male", "mic"];
右边的结构不仅仅是数组,只有是有Iterator接口的数据结构都可以。
解构赋值可以带有默认值,如果右边对应的值是undefined,默认值生效。
let [name="mic"] = ["mic2"];
let [name= "mic"] = [undefined];
(2) 对象解构赋值,同样对比以前的方式:
let obj = {
name:"mic",
city:"深圳",
sex:"male"
}
//ES5
let name = obj.name,
city = obj.city,
sex = obj.sex;
//ES6
let {name,city,sex} = obj;
还可以带上别名:
let {name:my_name,city:my_city,sex:my_sex} = obj;
console.log(my_name);//mic
console.log(my_city);//深圳
console.log(my_sex);//male
对象解构同样可以嵌套带默认值。
(3) 函数参数解构,为函数体内的参数赋值,也可以带默认值。
function reg([name,sex,city="深圳"]){
console.log(name);
console.log(sex);
console.log(city);
} reg(["mic","male"]);
//mic
//male
3.字符串/数组的扩展
3.1 字符串扩展比较实用的就是两个特性:模板字符串
(1)模板字符串:还记得以前多行字符串的拼凑么,现在实用`符号可以定义多行文本,并且可以嵌入变量。
//ES5
var name = "mic";
var str = "<div>" +
"this is a test,my name = " + name +
"</div>"; //ES6
let name = "mic;
let str = `<div>
this is a test,my name = ${name}
</div>`;
使用模板字符串显然方便很多。
3.2 数组扩展
(1) 扩展运算符:...符号可以把数组解析为逗号分隔的序列,也就是展开数组,常用语数组传递给函数参数。
(2) Array.from():将类数组转换为真正的数组,再也不需要使用拗口的Array.prototype.slice.call了。、
(3) Array.of():将一组值转为数组,用来替代new Array,因为new Array(3) 存在歧义,3代表数组长度,实际上我也可能只想创建一个数组只包含3,所以使用Array.of的行为更加规范。
(4) Array.copyWith/find/findIndex/includes 具体可以参考api
(5) Array.keys/values/entries 分别用来遍历下标,值以及下标和值组成的数组
4. Set/Map结构:
4.1 Set结构:Set结构是一种没有重复值的数组,当你在业务中需要用到去重时,Set最合适不过了。
(1) 创建Set:传递数组或者具有iterable接口的结构来创建。
const set = new Set([1, 2, 3, 4, 4]);
(2) 方法:size/add/delete/has/clear 具体见api
(3) 遍历:
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员
(4) 小技巧,数组去重:
function uniqArray(array_list){
return [...new Set(array_list)];
}
4.2 Map结构:Map的出现是为了Object键值只能是字符串的问题,Map出现以前,处理Hash值的数据结构只有对象,但是对象的键值只能是字符串,所以有局限性,而Map结构在Object的字符串基础上,可以设置各种类型的值,包括数字,字符串,对象等等都可以作为键值,极大的丰富了处理Hash值的功能。
(1) 创建/构造函数
二维数组,或者任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数
const map = new Map([
['name','mic'],
['city', 'shenzhen']
]);
(2) 方法:
size
set(key,value)
get(key)
has(key)
delete(key)
clear()
(3) 遍历:
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员
5. ArrayBuffer结构:
5.1 什么是ArrayBuffer结构:
ArrayBuffer是ES6新增的一种数据结构,对于以前浏览器场景而言,最常用的数据结构就是字符串,json字符串等文本格式,对于小数据量的业务已经足够了,但是在大数据量场景,使用字符串就会比较吃力,比如WebGL的处理,需要处理大量的数据,或者小程序蓝牙通信,需要传输声音数据等等,这种情况下字符串转换是需要大量时间的,而ArrayBuffer是一种二进制数据,可以让JS和设备之间之间进行二进制数据传输,无需转换。
二进制数据的操作主要由ArrayBuffer对象来存储,一个保存了内存某块区域的对象,通过TypeArray或DataView来访问,TypedArray是固定格式来访问,而DataView可以混合格式来访问。
5.2 ArrayBuffer对象:
(1)创建:ArrayBuffer对象是二进制数据的存储媒介,相当于数据源,获取和设置二进制数据最终是存储在这个对象里面,但是不能直接对他读取和保存,只能通过TypedArray或DataView来读写。
let buf = new ArrayBuffer(128);//创建一个128字节的内存区域,每个字节默认值0
if(buf.byteLength == 128){
//分配成功
}
else{
//异常处理
}
(2)属性和方法:
ArrayBuffer.prototype.byteLength: 返回内存字节长度
ArrayBuffer.prototype.slice(start,end): 拷贝出一个新的ArrayBuffer对象,从start开始到end
ArrayBuffer.isView():静态方法,判断一个对象是否是TypedArray
实例或DataView
实例
5.3 TypedArray视图
ArrayBuffer存储的数据可以有不同的视角来读取和保存,TypedArray视图就是来访问ArrayBuffer对象的方法之一,他通过某个固定的数据类型来访问,主要有以下几种类型:
Int8Array:8位有符号整数,长度1个字节。
Uint8Array:8位无符号整数,长度1个字节。
Uint8ClampedArray:8位无符号整数,长度1个字节,溢出处理不同。
Int16Array:16位有符号整数,长度2个字节。
Uint16Array:16位无符号整数,长度2个字节。
Int32Array:32位有符号整数,长度4个字节。
Uint32Array:32位无符号整数,长度4个字节。
Float32Array:32位浮点数,长度4个字节。
Float64Array:64位浮点数,长度8个字节。
构造函数的静态属性和实例的属性BYTES_PER_ELEMENT都可以获取该类型的字节数:
Int32Array.BYTES_PER_ELEMENT // let view = new Int32Array(8);
view.BYTES_PER_ELEMENT;//
(1) 创建:
TypedArray(ArrayBuffer buffer, [int start], [int length]]);//buffer为ArrayBuffer对象,start可选值,视图的开始字节,默认0,length结束的字节,默认全部
var buff = new ArrayBuffer(128);// 创建一个128字节的ArrayBuffer
var view = new Int32Array(buff);//创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
TypedArray(length);//直接通过指定长度来创建ArrayBuffer和View
let view = new Int32Array(8);
//等同于
let buf = new ArrayBuffer(4*8);
let view = new Int32Array(buf); view[0] = 10;
view[1] = 20;
view[2] = view[0] + view[1];
TypedArray(typedArray);//通过其他视图拷贝新视图,新的视图会重新创建内存
TypedArray(arrayLikeObject);//通过数组来创建
TypedArray类似于数组结构,数组的各种方法都可以用于TypedArray,也可以被遍历。
(2) 属性和方法:
TypedArray.prototype.buffer:返回整段内存区域对应的ArrayBuffer对象,属性为只读。
TypedArray.prototype.byteLength:返回TypedArray数组占据的内存长度,单位为字节,属性为只读。
TypedArray.prototype.byteOffset:返回TypedArray数组从底层ArrayBuffer对象的哪个字节开始,属性为只读。
TypedArray.prototype.length:TypedArray数组含有多少个成员。
TypedArray.prototype.set():用于复制数组(普通数组或TypedArray数组)。
TypedArray.prototype.subarray():对于TypedArray数组的一部分,再建立一个新的视图。
TypedArray.prototype.slice():返回一个指定位置的新的TypedArray实例。
TypedArray.of():用于将参数转为一个TypedArray实例。
TypedArray.from():返回一个基于这个结构的TypedArray实例。
(3)字符串和ArrayBuffer的转换
// ArrayBuffer转为字符串
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
// 字符串转为ArrayBuffer对象
function str2ab(str) {
var buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
var bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
5.4 DataView视图:要了解DataView视图的使用场景,需要了解计算机存储二进制数据的大端序和小段序问题,具体可以百度一下,总的来说,对于个人电脑和服务器存储二进制的格式顺序稍微有点差别。
对于跟网卡,声卡等本机设备进行通信,TypedArray视图是完全够用的,但是对于服务器获取其他网络设备传来的大端序数据,使用TypedArray就会出现问题,DataView就是为了解决这个问题的方法。
(1) 创建:
DataView(ArrayBuffer buffer,[int start],[length]);//同TypedArray
(2) 属性和方法
DataView实例有以下属性,含义与TypedArray实例的同名方法相同。
DataView.prototype.buffer:返回对应的ArrayBuffer对象
DataView.prototype.byteLength:返回占据的内存字节长度
DataView.prototype.byteOffset:返回当前视图从对应的ArrayBuffer对象的哪个字节开始
DataView实例提供8个方法读取内存。
getInt8:读取1个字节,返回一个8位整数。
getUint8:读取1个字节,返回一个无符号的8位整数。
getInt16:读取2个字节,返回一个16位整数。
getUint16:读取2个字节,返回一个无符号的16位整数。
getInt32:读取4个字节,返回一个32位整数。
getUint32:读取4个字节,返回一个无符号的32位整数。
getFloat32:读取4个字节,返回一个32位浮点数。
getFloat64:读取8个字节,返回一个64位浮点数。
DataView视图提供8个方法写入内存。
setInt8:写入1个字节的8位整数。
setUint8:写入1个字节的8位无符号整数。
setInt16:写入2个字节的16位整数。
setUint16:写入2个字节的16位无符号整数。
setInt32:写入4个字节的32位整数。
setUint32:写入4个字节的32位无符号整数。
setFloat32:写入4个字节的32位浮点数。
setFloat64:写入8个字节的64位浮点数。
var buffer = new ArrayBuffer(128);
var data_view = new DataView(buffer); var view1 = data_view.getUint8(0);
var view2 = data_view.getUint16(1,true); view1.setUint8(0,10);
View2.setUint16(1,20,true);
get和set方法都有第二个可选参数,用于指定是大端序还是小端序访问,默认是大端序,设置为true可指定为小端序。