- let声明
- const声明
- 块级作用域
- spread/rest
一、let声明与块作用域
在ES6之前,JavaScript中的作用域基本单元就是function。现在有了let就可以创建任意块的声明,也被称为作用域块。这意味者只需要“{}”就可以创建一个作用域。不再像var声明变量那样总归属于包含函数或全局。例如:
1 var a = 2; 2 { 3 let a = 3 4 console.log(a); //3 5 } 6 console.log(a); //2
在相关的资料中看到有介绍,let声明在被正式列入标准之前长这样:
1 //这种写法不合法,可以理解它为let语法的前身 2 let (a = 2, b, c){ 3 //... 4 }
这种写法从语法设计角度来说被称为显示声明,也就是说声明一个作用域块,而现在{let a = 1;}这种写法被称为隐式声明,这种声明被称为作用域劫持。我想选择隐式的声明方式不是没有原因的,比如let在for中的应用:
1 var func = []; 2 for(let a = 0; a < 5; a++){ 3 func.push(function(){ 4 console.log(a); 5 }); 6 } 7 func[3](); //3 (如果for内使用var声明a,打印结果会是5)
通过上面for的示例可以看到let在每次循环时都会劫持一个作用域,每次都会有一个独立的a变量。而不是像以前var会把变量提升到for所在的作用域,循环内使用公共的变量。这种块级作用域如果采用“let(){}”显示声明好像就无法处理了,当然形式不是重点,而是需要说明let会将它所在的块作为独立的一层作用域。但是在if条件语句中let的作用域劫持依然成立,但是if条件语句内产生的块级作用域内部出现的方法,会被if条件所在的作用域引用:
1 let something ;// true | false 2 if(something){ 3 let a = 10; 4 function foo(){ 5 console.log(a); 6 } 7 }else{ 8 let a = 20; 9 function foo(){ 10 console.log(a); 11 } 12 } 13 foo(); //这里会打印 10 或者 20 14 console.log(a); //这里报错,说明let会劫持条件语句的块,但foo执行正确只能说明foo的被if条件所在的作用域引用
剩下的while和do...while这些块都可以被let劫持,于for循环没有差异,就不逐个展示了。但是,还有一个重要的内容需要注意,有了作用域就必然会有作用域提升编译相关的底层逻辑值得关注。
let块及作用域的编译执行逻辑:
{ console.log(b); //报错:无法在初始化之前访问'b' let b = 20; }
这个异常提示说明let声明依然会提升,但是在没有赋值的情况下不会像var那样返回默认值undefined,而是不能访问。也就是说,let声明的变量必须在赋值后使用,没有默认值undefined。
通常也把这种异常称为“临时死区”。然后,到这里让我想到了上一篇的严格模式,如果在let劫持的作用域中出现了不严格的代码书写,会产生什么样的编译逻辑呢?
1 console.log(a); //a is not defined 2 { 3 console.log(a); //a is not defined 4 a = 10; 5 let b = 20; 6 console.log(a); // 10 7 } 8 console.log(a); // 10
显然,在let劫持的作用域下出现了未声明的变量赋值,会被默认处理成var声明,但这要发生在赋值时,这个原因是let虽然劫持作用域但是依然保持作用域链的特性,对当前作用域找不到的变量会向上层作用域遍历,直到全局作用域,如果没有找到还是会在全局作用域声明赋值。
最后还有一个Function在块级作用域中的编译:
1 { 2 fun(); //20 3 let a = 10; 4 function fun(){ 5 console.log(20); //在这段代码中这里不能是console.log(a),不然第二行代码会在执行时报错 6 } 7 }
在这个块级代码中的方法fun执行说明了方法在块级作用域中依然会被提升。但是需要注意,如果在fun中如果时打印a的值,以上的代码就会报错,因为出现了临时死区。除了以上的语法和编译值得我们探究,想更深入的了解let的实现原理,可以对比ES5的编译代码:
let声明的底层实现
let本质上起始就是块级作用域和var声明的严格模式,这里用上面if条件的示例来说明:
1 //ES6语法 2 { 3 let something = true; 4 if(something){ 5 let a = 10; 6 function foo(){ 7 console.log(a); 8 } 9 }else{ 10 let a = 20; 11 function foo(){ 12 console.log(a); 13 } 14 } 15 foo(); 16 } 17 //ES5编译结果 18 "use strict"; 19 { 20 var something = true; 21 22 if (something) { 23 var _foo = function _foo() { 24 console.log(a); 25 }; 26 27 var a = 10; 28 } else { 29 var _foo2 = function _foo2() { 30 console.log(_a); 31 }; 32 33 var _a = 20; 34 } 35 36 foo(); 37 }
这里可以可以思考以下for循环的let作用域劫持怎么编译成ES5的代码(还是使用前面for的示例):
1 //ES6的语法 2 var func = []; 3 for(let a = 0; a < 5; a++){ 4 func.push(function(){ 5 console.log(a); 6 }); 7 } 8 func[3](); 9 //ES5的编译结果 10 "use strict"; 11 12 var func = []; 13 14 var _loop = function _loop(a) { 15 func.push(function () { 16 console.log(a); 17 }); 18 }; 19 20 for (var a = 0; a < 5; a++) { 21 _loop(a); 22 } 23 24 func[3]();
二、const声明与块作用域
ES6中除了let可以实现块级作用域声明,还有const声明也可以实现块级作用域声明。const是用来声明常量的,也就是说声明必须有显式的初始化。如果需要undefined的常量,也是必须赋值undefined。
1 //ES6声明常量 2 { 3 const a = 10; 4 a = 20; //这里报错(看ES5中如何实现这种报错提示) 5 } 6 //ES5编译 7 "use strict"; 8 function _readOnlyError(name) { throw new Error(""" + name + "" is read-only"); } 9 { 10 var a = 10; 11 a = (_readOnlyError("a"), 20); 12 }
const声明常量的值不可以改变,是指不可以改变参数指向的栈内存的地址,如果时声明的常量时引用值类型的数据,可以改变对象的内部属性和方法。
1 //ES6语法声明引用值类型常量 2 { 3 const obj = { 4 a:10, 5 b:function(){} 6 } 7 obj.a = [1,2,3]; 8 obj.b = 40; 9 obj = {} 10 } 11 //ES5编译结果 12 "use strict"; 13 function _readOnlyError(name) { throw new Error(""" + name + "" is read-only"); } 14 15 { 16 var obj = { 17 a: 10, 18 b: function b() {} 19 }; 20 obj.a = [1, 2, 3]; 21 obj.b = 40; 22 obj = (_readOnlyError("obj"), {}); 23 }
最后,在这里添加一个与let和const无关的块级作用域问题,函数在独立的{}中也存在块级作用域概念(也可以说时严格模式下的函数表达式赋值),前面的if块级中起始已经有相关内容,函数的块级作用域只是作用在函数初始化在被执行执行时,在块级作用域内,函数的名称会以变量名的方式被提升到块级作用域外部,当块级作用域被执行过后才会真正挂载到外部的作用域对应的变量名称上,看下面这个编译结果就明白了:
1 //ES6块级作用域下的函数 2 { 3 let a = 10; 4 function fun(){console.log(a)} 5 } 6 fun(); 7 //ES5编译结果 8 "use strict"; 9 { 10 var _fun = function _fun() {//_fun变量提升,执行时挂载函数 11 console.log(a); 12 }; 13 var a = 10; 14 } 15 fun();
三、spread展开与rest收集
...展开&收集运算符
- 写:function test(...arg){}; test(1,2,3)收集作用
- 读:var arg = [1,2,3];console.log(...arg);展开作用
作用:简化书写长度,提升开发效率
ES6/ES7:ES6可以处理数组,ES7可以处理对象。
3.1应用展开替代apply:
1 function foo(x,y,z){ 2 console.log(x,y,z); 3 } 4 foo(...[1,2,3]); 5 //...的展开语法实际编译成ES5就是apply的应用,展开语法执行效率优于apply 6 "use strict"; 7 function foo(x, y, z) { 8 console.log(x, y, z); 9 } 10 foo.apply(void 0, [1, 2, 3]);
3.2应用展开向数组指定位置合并元素(替代concat):
1 var a = [1,2,3]; 2 var b = [0,...a,4,5]; 3 //ES5的编译结果 4 "use strict"; 5 var a = [1, 2, 3]; 6 var b = [0].concat(a, [4, 5]);
3.3应用收集解决实参与形参不对称问题:
1 function foo(a, b, ...c){ 2 console.log(a,b,c); 3 } 4 foo(1,2,3,4,5); //1 2 [3,4,5] 5 //ES5的编译结果 6 "use strict"; 7 function foo(a, b) { 8 for (var _len = arguments.length, c = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { 9 c[_key - 2] = arguments[_key]; 10 } 11 console.log(a, b, c); 12 } 13 foo(1, 2, 3, 4, 5); //1 2 [3,4,5]
3.4ES7中的...展开收集运算符实现对象属性浅层克隆:
1 var obj1 = { 2 a:"a", 3 b:"b", 4 c:[1,2,3] 5 } 6 var obj2 = { 7 e:"e", 8 f:{ 9 name:"F_NAME" 10 } 11 } 12 var obj = { 13 ...obj1, 14 ...obj2 15 } 16 console.log(obj); //{a: "a", b: "b", c: Array(3), e: "e", f: {…}} 17 obj.f.name = "f"; 18 console.log(obj2.f.name); //f
如果需要实现深克隆的话可以将子级拆分出来,然后分别展开收集:
1 var c = [1,2,3] 2 var obj1 = { 3 a:"a", 4 b:"b", 5 c:[...c] 6 } 7 var f = {name : "F_NAME"} 8 var obj2 = { 9 e:"e", 10 f:{ 11 ...f 12 } 13 } 14 var obj = { 15 ...obj1, 16 ...obj2, 17 c:[...c], 18 f:{...f} 19 } 20 // console.log(obj); 21 obj.f.name = "f"; 22 console.log(obj2.f.name); //F_NAME
其本质上就是采用了重新赋值的方式解决深克隆的需求。但是这种解决方案显然不适合层级较多的对象来实现,还可以使用JSON对象的方法来解决这类深克隆的问题:
1 var obj1 = { 2 a:"a", 3 b:"b", 4 c:[1,2,3] 5 } 6 var obj2 = { 7 e:"e", 8 f:{ 9 name:"F_NAME" 10 } 11 } 12 var obj3 = { 13 ...obj1, 14 ...obj2 15 } 16 var obj = JSON.parse(JSON.stringify(obj3)) 17 console.log(obj); 18 obj.f.name = "f"; 19 console.log(obj2.f.name); //F_NAME
采用JSON对象方法的这种深度克隆解决方案需要注意的是在方法中不能出现Function类型的属性,如果有这种情况,就只能采用递归的方式实现深度克隆了。