zoukankan      html  css  js  c++  java
  • 深入理解 ES6(摘要)

    JavaScript 并不是(或者说不再是)一门“玩具”语言了。所以不,本文主要聚焦于 ES6 的一些 new feature,以及表达一些自我的理解。

    最佳实践,使用 let/const

    块级作用域的理解

    在 JavaScript 中,很多人会把块级作用域函数作用域分离开来。其实,块级作用域(也叫词法作用域)包含两块:

    • 其一就是函数作用域,在函数内部
    • 其二就是块,即 {} 中间

    所以,JavaScript 引入 let/const 就是为了解决第二类块级作用域的问题。

    temporal dead zone

    涉及到 let/const 的具体实现方面,会接触到暂时性死区 (TDZ) 这个概念。首先,把我的结论亮出来:使用 let/const 声明的变量存在 hoisting。

    在编译器在扫描代码发现变量声明时,遇到 var 声明的变量,就将它们提升至作用域顶部,遇到 let/const 声明的变量则放入 TDZ 中。TDZ 在变量声明后终结,而不是赋值后。在块级作用域结束时,变量立刻被垃圾回收器回收。我们看下面这个例子:

    let x = 'outer value';
    (function() {
      console.log(x); // Reference error
      let x = 'inner value';
    }());

    这里,如果 let 声明的变量不提升的话,会输出 ‘outer value’,但实际输出会报错。这就是说 lex x = 'inner value' 会被提升,但在声明前无法被访问(放入 TDZ 区域)。

    //console.log(aLet); //Reference error
    let yuer;
    console.log(yuer); // undefined
    yuer = 10;
    console.log(yuer); // 10
    

    上面这段代码是解释 TDZ 的一个 Life cycle。

    
    

    再看一个例子:

    
    
    let a = f();
    const b = 2;
    function f() { console.log (b); } //Reference error
    
    

    不允许重复声明

    
    

    在使用 var 的时代中,我们可以写出下面的代码:

    
    
    var xiaoke = 2;
    var xiaoke = 3;
    console.log(xiaoke);// 3
    
    
    

    在严格模式下,这段代码是不报错的。但是 let/const 不允许这种行为发生。也就是下面的代码都会报错。

    
    
    'use strict';
    let xiaoke = 1;
    var xiaoke = 2;
    console.log(xiaoke); //SyntaxError
    
    let love = 1;
    let love = 2;
    console.log(love); //SyntaxError
    
    var yuer = 1;
    let yuer = 2;
    console.log(yuer); //SyntaxError
    
    
    

    const 也是同理。但是注意的是,如果内嵌另一个作用域,便可在内嵌的作用域中用 let 声明同名变量。

    
    
    let count = 2;
    if (condition){
        let count = 3; //对的
    }
    
    
    

    const 用来声明对象

    
    

    const 用来表示一个常量,一般来说,我们该为常量使用大写命名,但这适用于一些在执行前就已知的值。对于在执行期间实时计算出来的不变的值,最好使用常规命名。

    
    

    需要注意的是,const 声明不允许修改绑定,但允许修改值。也就是说,对于引用类型,引用是无法修改的,但所指向的内容是可以的。

    
    
    'use strict';
    const person = {
        name: 'yuer'
    };
    
    person.name = 'xiaoke'; // 对
    
    /*
    person = {
        name: ‘xiaoke' // SyntaxError
    }*/
    
    
    

    我们可以这样来验证:

    
    
    'use strict';
    const person = {
        name: 'yuer'
    };
    console.log(Object.getOwnPropertyDescriptor(person,'name'));
    /*
    { value: 'yuer',
      writable: true,
      enumerable: true,
      configurable: true }
    */
    
    
    

    全局作用域使用 let/const

    
    

    全局作用域内使用 let/const 和 var 的一个区别是,var 创建的变量会成为全局对象(通常是浏览器,即 window 对象)的一个属性,也意味着会可能会无意中覆盖已经存在的全局变量。

    
    

    如果使用 let/const,不会自动添加为全局对象的属性。

    
    

    可以更新的写法

    
    

    if/while/for/for…of/for…in 循环

    
    

    使用 let 代替 var,这没什么好说的。

    
    

    保护现场

    
    

    在循环中使用函数,经常我们会采用 IIFE 来保护现场。

    
    
    for (var i = 0; i< 10; ++i){
        setTimeout((function (value) {
            return function () {
                console.log(value);
            }
        })(i),100);
    }
    
    
    

    有了 let/const,就不需要这么折腾了。

    
    
    for (let i = 0; i< 10; ++i){
        setTimeout(function () {
            console.log(i);
        },100);
    }
    
    
    

    for…in/for…of 中 const 的异常表现

    
    

    const 一般不会用在 for 循环中,比如:

    
    
    for (const i = 0; i< 10; ++i){
        //do something
    }
    
    
    

    i 被声明为常量,在 for 循环中会面临修改。因此会抛出错误。

    
    

    但在 for…in 和 for…of 中使用不会产生错误:

    
    
    var obj = {
        name: 'yuer',
        age:22
    };
    for (const key in obj){
        console.log(key);
    }
    
    
    

    这是因为 for…in/for…of 中,每次迭代不会试图修改已有绑定,而是创建一个新绑定。但是如果你试图修改 key 的值,则会抛出错误:

    
    
    var obj = {
        name: 'yuer',
        age:22
    };
    for (const key in obj){
        key = Math.random().toString(); //TypeError
        console.log(key);
    }
    
    
    

    当然了,const 的安全性更强,所以社区的一个做法日益普及:默认使用 const,只有确实需要改变变量的情况下使用 let。

    
    
    
    

    函数的性能优化

    
    

    ES6 对函数进行了大量改进。

    
    

    携带默认参数的函数

    
    

    以前的写法中,通常使用 || 运算符来提供参数默认值。

    
    
    function foo(url, timeout, method){
        timeout = timeout || 2000;
        //do something
    }
    
    
    

    有一个弊端就是,如果我们给 timeout 传入 0timeout 也会采用默认值 2000。我们可以通过下面的方法来解决:

    
    
    function foo(url, timeout, method){
        timeout = (typeof timeout !== "undefined") ? timeout || 2000;
        //do something
    }
    
    
    

    ES6 对这一操作进行了改进。

    
    

    默认参数值

    
    
    function foo(url, timeout = 2000, method = function () {return 'xiaoke'}){
        console.log(url, timeout,method());
    }
    foo();                                      //(1) undefined 2000 'xiaoke'
    foo('http://xiaokedada.com');               //(2) http://xiaokedada.com 2000 xiaoke
    foo('http://xiaokedada.com', 400);          //(3) http://xiaokedada.com 400 xiaoke
    foo('http://xiaokedada.com', undefined,function bar(){return 'yuer'});  //(4) http://xiaokedada.com 2000 yuer
    foo('http://xiaokedada.com', null,function bar(){return 'yuer'});       //(5) http://xiaokedada.com null yuer
    
    
    

    总结一下:

    
    
    • 使用 = 给参数添加默认值
    • 需要给参数主动传入 undefined,才会使用默认值(示例4)。null 是个合法值(示例5)。
    
    

    默认参数表达式

    
    
    function getValue(x) {
        return x + 5;
    }
    function foo(first, second = getValue(first)){
        console.log(first + second);
    }
    foo (1);    //7
    foo(1,1);   //2
    
    
    

    上式表示,第二个参数不传入,会默认使用表达式计算出的值作为 second 的默认参数值。

    
    

    但如果下面这样呢? first 依赖 second 取得默认值的情况下,给 first 传递 undefined

    
    
    function getValue(x) {
        return x + 5;
    }
    function foo(first = getValue(second), second){
        console.log(first + second);
    }
    foo(undefined,1);   //ReferenceError
    
    
    

    这涉及到暂时性死区 (TDZ) 的问题,上述的调用可以想象成:

    
    
    let first = getValue(second);   //访问 TDZ 中的变量,错误
    let second = 1;
    
    
    

    函数参数有自己的作用域和暂时性死区,其与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。

    
    

    arguments 的怪异之处

    
    

    非严格模式下:

    
    
    function mix(first, second) {
        console.log(first === arguments[0]);    //true
        console.log(second === arguments[1]);   //true
        first = 'c';
        second = 'd';
        console.log(first === arguments[0]);    //true
        console.log(second === arguments[1]);   //true
    }
    mix('a','b');
    
    
    

    在这种情况下,函数参数的变化会同步更新到 arguments 对象中。在严格模式中,取消了这种怪异方式。

    
    
    'use strict';
    function mix(first, second) {
        console.log(first === arguments[0]);    //true
        console.log(second === arguments[1]);   //true
        first = 'c';
        second = 'd';
        console.log(first === arguments[0]);    //false
        console.log(second === arguments[1]);   //false
    }
    mix('a','b');
    
    
    

    ES6 函数默认参数值的行为与 ES5 严格模式 arguments 保持一致。

    
    
    function mix(first = 'c', second='d') {
        console.log(arguments.length);
        console.log(first === arguments[0]);
        console.log(second === arguments[1]);
    }
    mix('a','b');   //2 true true
    mix('a');       //1 true false
    mix();          //0 false false
    mix(undefined,'b'); //2 false true
    
    
    

    以第二个为例,和下面的代码是一致的。

    
    
    function mix(first, second) {
        second = 'd';
        console.log(arguments.length);
        console.log(first === arguments[0]);
        console.log(second === arguments[1]);
    }
    mix('a');
    
    
    

    rest 参数

    
    
    function foo(first,...keys){
        for(let item of keys){
            console.log(item);
        }
    }
    foo('a','b','c','d');   //b c d
    
    
    

    用法很简单:rest 参数 keys 包含了 first 之后传入的所有参数。引入 rest 参数的设计初衷是替代 arguments。即这样:

    
    
    function foo(...keys){
        //do something
    }
    
    
    

    rest 参数的限制是:

    
    
    function foo(first,...keys,last){
        //do something
    }
    
    
    

    这种写法会抛出错误,也是不被允许的。

    
    
    
    

    Spread 运算符

    
    

    ... 运算符作用于数组操作,将数组元素分离成为单独的元素。

    
    

    在函数调用时,我们也用到这个 ... 来表示 rest 参数

    
    
    console.log(...[1,2,3]);    //1 2 3
    
    
    

    用来替换 apply

    
    

    比如,下面这个例子求数组元素的最大值:

    
    
    let values = [3,5,10,34,-1];
    console.log(Math.max.apply(Math, values));  //34
    
    
    

    这是因为 Math.max(arg1,arg2,arg3,...) 只能处理分离的数字,我们可以巧妙地使用 ... 操作符。

    
    
    let values = [3,5,10,34,-1];
    console.log(Math.max(...values));   //34
    
    
    

    将 ... 运算符和数组的结合视为一般参数的话,能更多地扩展它的功能。比如:

    
    
    let values = [3,5,10,34,-1];
    console.log(Math.max(...values,50));    //50
    
    
    

    再举个例子:

    
    
    let values = [3,5,10];
    function foo(x,y,z) {
        return x+y+z;
    }
    //console.log(foo.apply(null, values));
    console.log(foo(...values));    // 终于可以不用该死的 null
    
    
    

    将类数组转换为数组

    
    

    以前,我们通常使用 Array.prototype.slice.call() 将 NodeList 和 arguments (类数组) 转化为数组。现在我们有一个更简单的方法。

    
    
    function foo(x,y,z) {
        console.log(Array.isArray(arguments]);  //false
        console.log(Array.isArray([...arguments])); //true
    }
    foo(1,2,3);
    
    
    

    合并和复制数组

    
    
    //example 01
    let arr1 = [3,5,10];
    let arr2 = [];
    arr2.push(...arr1);   //[3,5,10]
    
    //example 02
    let arr1 = ['two', 'three'];
    let arr2 = ['one', ...arr1, 'four', 'five'];    //["one", "two", "three", "four", "five"]
    
    //example 03
    let arr1 = [1,2,3];
    let arr2 = [...arr1];
    arr2.push(4);
    console.log(arr1);  //[1,2,3]
    console.log(arr2);  //[1,2,3,4]
    
    //example 04
    let arr1 = [1,2,3];
    let arr2 = arr1;
    arr2.push(4);
    console.log(arr1);  //[1,2,3,4]
    console.log(arr2);  //[1,2,3,4]
    
    
    

    尝试比较例子 3 和例子 4,分别是深度拷贝和浅拷贝。

     
  • 相关阅读:
    【Java8新特性】你知道Java8为什么要引入Lambda表达式吗?
    你说你懂计算机网络,那这些你都知道吗
    程序员需要了解的硬核知识之控制硬件
    程序员需要了解的硬核知识之汇编语言(全)
    真的,Kafka 入门一篇文章就够了
    程序员需要了解的硬核知识之汇编语言(一)
    带你涨姿势的认识一下 Kafka 消费者
    程序员需要了解的硬核知识之操作系统和应用
    详解Kafka Producer
    程序员需要了解的硬核知识之操作系统入门
  • 原文地址:https://www.cnblogs.com/liubingchen/p/8334339.html
Copyright © 2011-2022 走看看