Node v4这个版本是Node和iojs合并后发布的首个稳定版本,并且为开发者带来了大量的ES6语言扩展。

  • 严格模式可以应用到整个script标签或某个别函数中。let允许把变量的作用域限制在块级域中。与var不同处是:var申明变量要么是全局的,要么是函数级的,而无法是块级的。
  • const这个声明创建一个常量,可以全局或局部的函数声明。常量遵循与变量相同的作用域规则。一个常量不可以被重新赋值,并且不能被重复声明。一个常量不能和它所在作用域内的其他变量或函数拥有相同的名称。
  • 块级作用域:很多语言中都有块级作用域,JavaScript使用var声明变量,以function来划分作用域,大括号“{}”却限定不了var的作用域。用var声明的变量具有变量提升(declaration hoisting)的效果。ES6里增加了一个let,可以在{}、if、for里声明。用法同var,但作用域限定在块级,let声明的变量不存在变量提升。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    'use strict';
    function f1() {
    var a = 1;
    let n = 2;
    if (true) {
    var a = 20;
    let n = 10;
    }
    console.log(n); // 2
    console.log(a); // 20
    }
    f1();
  • extends关键字可以用来创建继承。

  • ES6中的类实际上就是个函数,而且正如函数的定义方式有函数声明和函数表达式两种一样,类的定义方式也有两种,分别是:类声明、类表达式。类的成员需要定义在一对花括号{}里,花括号里的代码和花括号本身组成了类体。类成员包括类构造器和类方法(包括静态方法和实例方法)。一个类只能有一个constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。static关键字定义了一个类的静态方法。静态方法被称为无需实例化类也可当类被实例化。静态方法通常用于为应用程序创建实用函数。
  • map对象是一个简单的键/值映射。任何值(包括对象和原始值)都可以用作一个键或一个值。初始化Map需要一个二维数组,或者直接初始化一个空Map。注意Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。1、如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。2、如果Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map将其视为一个键。
  • map实例的属性和操作方法、map遍历方法。
  • ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。向Set加入值的时候,不会发生类型转换,所以5和”5”是两个不同的值。Set内部判断两个值是否不同,使用的算法类似于精确相等运算符(===),这意味着,两个对象总是不相等的。
  • 遍历Array可以采用下标循环,遍历Map和Set就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,Array、Map和Set都属于iterable类型。具有iterable类型的集合可以通过新的for…of循环来遍历。for…of循环是ES6引入的新的语法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var a = ['A', 'B', 'C'];
    a.name = 'hello';

    for (var x in a) { // 遍历Array
    console.log(x); // A B C hello (会输出hello)
    }
    for (var x of a) { // 遍历Array
    console.log(x); // A B C
    }

    var s = new Set(['A', 'B', 'C']);

    for (var x of s) { // 遍历Set
    console.log(x); // A B C
    }

    var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);

    for (var x of m) { // 遍历Map
    console.log(x[0] + '=' + x[1]); // 1=x 2=y 3=z
    }

  • Map原生提供三个遍历器生成函数和一个遍历方法。1、keys():返回键名的遍历器。 2、values():返回键值的遍历器。 3、entries():返回所有成员的遍历器。 4、 forEach():遍历Map的所有成员。

  • generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。形式上,Generator函数是一个普通函数,但是有两个特征。一是,function命令与函数名之间有一个星号;二是,函数体内部使用yield语句,定义不同的内部状态(yield语句在英语里的意思就是“产出”)。调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
  • 因为generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数(保存状态)。generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。
  • 调用generator对象有两个方法,一是不断地调用generator对象的next()方法;第二个方法是直接用for…of循环迭代generator对象,这种方式不需要我们自己判断done。
  • 箭头函数就是个是简写形式的函数表达式,并且它拥有词法作用域的this值。箭头函数总是匿名的。箭头函数的一个用处是简化回调函数。this对象的指向是可变的,但是在箭头函数中,它是固定的。函数体内的this对象,绑定定义时所在的对象,而不是使用时所在的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var f = v => v;
    //等同于:
    var f = function(v) {
    return v;
    };

    // 正常函数写法
    [1,2,3].map(function (x) {
    return x * x;
    });
    // 箭头函数写法
    [1,2,3].map(x => x * x);
  • 模板字符串使用反引号来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression})的占位符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //标签函数的第一个参数是一个包含了字符串字面值的数组(在本例中分别为“Hello”和“world”);第二个参数,在第一个参数后的每一个参数,都是已经被处理好的替换表达式(在这里分别为“15”和“50”)。最后,标签函数返回处理好的字符串。
    var a = 5;
    var b = 10;

    function tag(strings) {
    console.log(arguments); // { '0': [ 'Hello ', ' world ', '' ], '1': 15, '2': 50 }
    console.log(strings[0]); // "Hello "
    console.log(strings[1]); // " world "
    console.log(arguments[1]); // 15
    console.log(arguments[2]); // 50
    return "Hubwiz!";
    }

    console.log(tag`Hello ${ a + b } world ${ a * b}`); //注:无括号
  • 字符串的扩展:JavaScript共有6种方法可以表示一个字符。

    1
    2
    3
    4
    5
    '\z' === 'z'  // true
    '\172' === 'z' // true
    '\x7A' === 'z' // true
    '\u007A' === 'z' // true
    '\u{7A}' === 'z' // true
  • 传统上,JavaScript的字符串只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法:includes()、startsWith()、endsWith()。repeat方法返回一个新字符串,表示将原字符串重复n次。

  • 重命名导出和导入:有时,导入的变量名碰巧与你需要使用的一些变量名冲突了,ES6允许你重命名导入的变量。
    1
    2
    import {flip as flipOmelet} from "eggs.js";
    import {flip as flipHouse} from "real-estate.js";

同样,你在导出变量的时候也可以重命名它们。这在你想使用不同名字导出相同功能的时候十分方便。

1
2
3
4
5
6
7
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};

  • CommonJS是一个单对象输出,单对象加载的模型,ES6 module是一个多对象输出,多对象加载的模型。
  • 在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。
  • ES6的模块自动开启严格模式,即使你没有写”use strict”;在模块中,你可以使用import和export。你可以export任何的顶级变量:function、class、var、let、const。
  • 除了导出,模块里的代码和其他普通代码没有什么区别。它可以访问全局变量,像Object和Array。如果你的模块在浏览器运行,还能够使用document和XMLHttpRequest。由于这个代码是一个模块,而不是一个脚本,所有的声明的作用域都只属于这个模块,而不是所有脚本和模块都能全局访问的。你只要把模块中的声明导出成一组公共模块的API就足够了。
  • 当你运行一个包含import声明的模块,会先导入要导入的模块并加载,然后根据深度优先的原则遍历依赖图谱来执行对应模块,并跳过已经执行的模块,来避免循环。
  • 你可以把你要导出的功能名写在一个列表里,然后用大括号括起来,这样就不用在每个要导出的功能前面加上export标记。你可以有多个导出列表,或者将导出列表与导出声明混合使用,只要不重复导出同一个变量名就行。
  • 重命名导出和导入:有时,导入的变量名碰巧与你需要使用的一些变量名冲突了,ES6允许你重命名导入的变量。
    import {flip as flipOmelet} from “eggs.js”;
    import {flip as flipHouse} from “real-estate.js”;
    同样,你在导出变量的时候也可以重命名它们。这在你想使用不同名字导出相同功能的时候十分方便。
    function v1() { … }
    function v2() { … }
    export {
    v1 as streamV1,
    v2 as streamV2,
    v2 as streamLatestVersion
    };

  • 默认的导出:如果你希望自己ES6模块也具有默认导出,这很简单。默认的导出方式并没有什么魔力;他就像其他导出一样,除了它的导出名为default。
    let myObject = {
    field1: value1,
    field2: value2
    };
    export {myObject as default};
    或者使用简写:
    export default {
    field1: value1,
    field2: value2
    };

  • JavaScript Loader的相关标准的工作也正在进行中,预计在HTML中将会被添加类似<script type=module>这样的东西。
  • 模块和脚本差别大主要体现在模块的解析和加载上:模块并不是必须使用import和export的。有的模块可能根本不需要输入或者输出什么,比如要在全局作用域内修改什么东西。总之,出现import和export可以表明是个模块,没有import和export并不能清楚地判定一个文件不是模块。所以在解析的时候没有行之有效的办法来自动检测一个文件是个模块。
    加载的差异:当一个模块加载完毕,import语句会触发指定文件的加载。引入的文件必须在解析和加载(没有报错)完成后,模块才开始执行。为了尽可能地提升速度,加载(【译者注】这里是指引入文件的加载)在import语句解析完成后就开始了,要早于模块剩余部分的解析。依赖文件加载完成之后,还有额外的一个步骤,就是确认引入的绑定确实存在于依赖文件中。如果你从foo.js模块引入了foo,在继续执行之前,JavaScript引擎需要验证foo,js确实输出了foo。
  • 静态import解决不了的:1、根据需要导入模块(或有条件) 2、在运行时计算模块标识符 3、从一个普通的脚本中导入一个模块(而不是一个模块)
  • 动态import():import(moduleSpecifier)在获取,实例化并评估所有模块的依赖关系后(包扩这个模块本身),返回这个请求模块的命名空间的Promise对象。
  • 静态import和动态import()都非常有用,都有自己独特的用处。使用静态的import来初始绘制依赖关系,特别是对于上面的内容。 在其他情况下,请考虑使用动态的import()加载依赖关系。
  • 如果想运行上面的例子,需要返回正确的MIME类型:Failed to load module script: The server responded with a non-JavaScript MIME type of “application/octet-stream”. Strict MIME type checking is enforced for module scripts per HTML spec.
  • ES6、CJS两个模块系统的语法非常类似,但是语义是完全不同的。在一些细节方面需要特殊的处理来实现100%的规范方面的兼容。
  • Bable实现了ES6模块,但是准确上来说,它没有实现所有的规范。如果你正在使用Babel来转义一个原生的ES6模块的时候,请当心,这可能会有某些副作用。
  • 虽然foo和bar在两个语句中加载,但是它们对应的是同一个my_module实例。也就是说,import语句是 Singleton 模式。

    1
    2
    3
    4
    import { foo } from 'my_module';
    import { bar } from 'my_module';
    // 等同于
    import { foo, bar } from 'my_module';
  • 使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。这时import命令后面,不使用大括号。

  • export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法。本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。
  • import命令会被JavaScript引擎静态分析,先于模块内的其他模块执行(叫做”连接“更合适)。import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,才会加载指定的模块。import()类似于Node的require方法,区别主要是前者是异步加载,后者是同步加载。import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
  • ES6模块加载的机制,与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝(值类型是拷贝,引用类型是引用),而ES6模块输出的是值的引用(值类型,引用类型都是引用)。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。最后,export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
  • 浏览器对于带有type=”module”的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在ES6模块之中。
  • Node对ES6模块的处理比较麻烦,因为它有自己的CommonJS模块格式,与ES6模块格式是不兼容的。目前的解决方案是,将两者分开,ES6模块和CommonJS采用各自的加载方案。Node要求ES6模块采用.mjs后缀文件名。也就是说,1、只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。2、require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,3、.mjs文件里面也不能使用require命令,必须使用import。最后,Node 的import命令是异步加载,这一点与浏览器的处理方法相同。
  • ES6模块应该是通用的,同一个模块不用修改,就可以用在浏览器环境和服务器环境。为了达到这个目标,Node规定ES6模块之中不能使用CommonJS模块的特有的一些内部变量。
  • CommonJS模块的输出都定义在module.exports这个属性上面。Node 的import命令加载 CommonJS 模块,Node 会自动将module.exports属性,当作模块的默认输出,即等同于export default xxx。
  • CommonJS模块加载ES6模块,不能使用require命令,而要使用import()函数。ES6模块的所有输出接口,会成为输入对象的属性。
  • 模块加载机制必须考虑“循环加载”的情况。对于JavaScript语言来说,目前最常见的两种模块格式 CommonJS 和 ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。
  • CJS 是一个动态模块系统,ESM 是静态模块系统。浏览器只关心 MIME 类型,而不是文件扩展名。

Comments

去留言
2017-02-10

⬆︎TOP