块级作用域

ES6 中的 let 实际上才第一次引入了块级作用域这个概念,ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景:

  1. var tmp = new Date();
  2. function f() {
  3. console.log(tmp);
  4. if (false) {
  5. var tmp = "hello world"; // 变量定义提升: var tmp; ... if (false) tmp = "hello world";
  6. }
  7. }
  8. f(); // undefined

变量泄露为全局变量:

  1. var s = 'hello';
  2. for (var i = 0; i < s.length; i++) {
  3. console.log(s[i]);
  4. }
  5. console.log(i); // 5

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数:

  1. // ES6严格模式
  2. 'use strict';
  3. if (true) {
  4. function f() {} // ES5 会报错
  5. }
  6. // 不报错

自测,ES5 及 ES6 输出结果是什么?为什么?

  1. function f() { console.log('I am outside!'); }
  2. (function () {
  3. if(false) {
  4. // 重复声明一次函数f
  5. function f() { console.log('I am inside!'); }
  6. }
  7. f();
  8. }());

由于 ES5 跟 ES6 对于块级作用域这个概念的差别,导致了 if 中的函数定义这一句作为一个语句块能否被外层读取,块级作用域是封闭的,最终输出的结果很好的体现了块级作用域这一概念。

反撇号跟模板字符串

反撇号`在键位 Esc 下方,能跟’和”一样普通地用于表示字符串,但相比之下多了可以向字符串中插入值的功能,代替 + 运算符的连接作用,反撇号括起来的字符串中可以以 ${变量名} 的形式去插入变量,这样的字符串叫做模板字符串。举个小例子。

  1. var name = "豆浆";
  2. var age = 3;
  3. alert(`欢迎,${name}同学,你今年${age}岁了!`);

输出的结果自己体验吧~

${} 这个东西叫做占位符,它不仅可以放变量,还有函数或是算数运算等等都可以放进去,即 ${函数名},这样子就可以通过函数实时改变需要输出的信息,而且比起用 + 运算符来连接要更加简洁方便。后排提示,一定要注意使用英文的符号,不能用中文符号!

模板字符串不仅仅是用于代替连接作用,还有一些小特性。模板字符串在使用时可以互相嵌套,这种方法被称为模板套构,在模板字符串中如果再次出现反撇号`或是 $ 和 {} 等符号时跟”和’类似的,要使用转义字符 \。模板字符串插值实际上转换为字符串并输出的一个过程。

关于模板字符串的多行显示方式,支持 ES6 模板字符串的浏览器自然支持字符串不需要 \n 的换行方式,反撇号具备有’和”的基本功能,所以多行字符串的显示方式是相似的。

let/const

基本内容

  • let: 声明变量
  • const: 声明常量
  • let/const: 块级作用域
  • let命令、const命令声明的全局变量,不属于全局对象的属性

let 示例:

  1. var a = [];
  2. for (let i = 0; i < 10; i++) {
  3. a[i] = function () {
  4. console.log(i);
  5. };
  6. }
  7. a[6](); // 6

var 示例:

  1. var a = [];
  2. for (var i = 0; i < 10; i++) {
  3. a[i] = function () {
  4. console.log(i);
  5. };
  6. }
  7. a[6](); // 10

暂时性死区(TDZ)

只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。对于 let 不会有变量提升,但是在 let 所在的语句块会检测有无 let,如果调用 let 变量在声明前的话会报错,即后面所说的暂时性死区。

  1. var tmp = 123;
  2. if (true) {
  3. tmp = 'abc'; // ReferenceError
  4. let tmp;
  5. }

上面代码中,存在全局变量 tmp,但是块级作用域内 let 又声明了一个局部变量 tmp,导致后者绑定这个块级作用域,所以在 let 声明变量前,对 tmp 赋值会报错。在同一语句块中不能对同一 let 变量,用 let 又用 var 定义,函数的形参也不能在函数体内定义为 let,默认已经定义好了,再用 let 也会变为重复声明,同一语句块中同名变量不能反复声明。

ES6 明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为’’’“暂时性死区”(temporal dead zone,简称TDZ)’’’。

  1. if (true) {
  2. // TDZ开始
  3. tmp = 'abc'; // ReferenceError
  4. console.log(tmp); // ReferenceError
  5. let tmp; // TDZ结束; 若使用 var 那么由于变量定义提升,开始的 tmp 不会报错
  6. console.log(tmp); // undefined
  7. tmp = 123;
  8. console.log(tmp); // 123
  9. }

相同作用域内不能重复声明

  1. // 报错
  2. function () {
  3. let a = 10;
  4. var a = 1;
  5. }
  6. // 报错
  7. function () {
  8. let a = 10;
  9. let a = 1;
  10. }

因此,不能在函数内部重新声明参数。

  1. function func(arg) {
  2. let arg; // 报错
  3. }
  4. function func(arg) {
  5. {
  6. let arg; // 不报错
  7. }
  8. }

第二段代码不报错是因为新创建了一段语句块,新的 arg 与形参 arg 不在同一语句块中,没有冲突。

变量的解构赋值

数组

基本形式:

  1. let [foo, [[bar], baz]] = [1, [[2], 3]];
  2. foo // 1
  3. bar // 2
  4. baz // 3
  5. let [ , , third] = ["foo", "bar", "baz"];
  6. third // "baz"
  7. let [x, , y] = [1, 2, 3];
  8. x // 1
  9. y // 3
  10. let [head, ...tail] = [1, 2, 3, 4];
  11. head // 1
  12. tail // [2, 3, 4]
  13. let [x, y, ...z] = ['a'];
  14. x // "a"
  15. y // undefined
  16. z // []

只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值:

  1. function* fibs() {
  2. var a = 0;
  3. var b = 1;
  4. while (true) {
  5. yield a;
  6. [a, b] = [b, a + b];
  7. }
  8. }
  9. var [first, second, third, fourth, fifth, sixth] = fibs();
  10. sixth // 5

解构赋值允许指定默认值,ES6 内部使用严格相等运算符(===),判断一个位置是否有值:

  1. var [foo = true] = [];
  2. foo // true
  3. var [x = 1] = [undefined];
  4. x // 1
  5. var [x = 1] = [null];
  6. x // null

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值:

  1. function f() {
  2. console.log('aaa');
  3. }
  4. let [x = f()] = [1];
  5. // 等价于:
  6. let x;
  7. if ([1][0] === undefined) {
  8. x = f();
  9. } else {
  10. x = [1][0];
  11. }

对象的解构赋值

示例:

  1. var { bar, foo } = { foo: "aaa", bar: "bbb" };
  2. foo // "aaa"
  3. bar // "bbb"
  4. //---
  5. var { foo: baz } = { foo: "aaa", bar: "bbb" };
  6. baz // "aaa"
  7. foo // error: foo is not defined
  8. //---
  9. var obj = {
  10. p: [
  11. "Hello",
  12. { y: "World" }
  13. ]
  14. };
  15. var { p: [x, { y }] } = obj;
  16. x // "Hello"
  17. y // "World"
  18. // 解构赋值时,如果等号右边是数值和布尔值,则会先转为对象
  19. let {toString: s} = 123;
  20. s === Number.prototype.toString // true
  21. //函数的参数也可以使用解构赋值
  22. [[1, 2], [3, 4]].map(([a, b]) => a + b);
  23. var map = new Map();
  24. for (let [key, value] of map) {
  25. console.log(key + " is " + value);
  26. }
  27. const { SourceMapConsumer, SourceNode } = require("source-map");

数组扩展

Array.from

Array.from 方法用于将两类对象转为真正的数组:类似数组(必须有 length 属性)的对象(array-like object)和可遍历(iterable)的对象:

  1. function foo() {
  2. var args = Array.from(arguments);
  3. // ...
  4. }
  5. Array.from({ length: 3 }); // [undefined, undefined, undefined]

扩展运算符(…)也可以将某些数据结构转为数组;扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换:

  1. function foo() {
  2. var args = [...arguments];
  3. }

Array.from 还可以接受第二个参数,作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组:

  1. Array.from(arrayLike, x => x * x); // Array.from(arrayLike).map(x => x * x);

Array.of

将一组值,转换为数组:

  1. Array.of(3, 11, 8) // [3,11,8]

find/findIndex

  1. [1, 4, -5, 10].find((n) => n < 0)
  2. [1, 5, 10, 15].find(function(value, index, arr) {
  3. return value > 9;
  4. }) // 10
  5. [1, 5, 10, 15].findIndex(function(value, index, arr) {
  6. return value > 9;
  7. }) // 2

函数扩展

默认参数

  1. function Point(x = 0, y = 0) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. function fetch(url, { body = '', method = 'GET', headers = {} }){
  6. console.log(method);
  7. }
  8. (function (a, b, c = 5) {}).length // 2 -> 函数的length属性,将返回没有指定默认值的参数个数

如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域:

  1. var x = 1;
  2. function f(x, y = x) {
  3. console.log(y);
  4. }
  5. f(2); // 2
  6. let foo = 'outer';
  7. function bar(func = x => foo) {
  8. let foo = 'inner';
  9. console.log(func()); // outer
  10. }
  11. bar();
  12. // 参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中
  13. // 的函数就不会运行)
  14. function throwIfMissing() {
  15. throw new Error('Missing parameter');
  16. }
  17. function foo(mustBeProvided = throwIfMissing()) {
  18. return mustBeProvided;
  19. }
  20. foo();

rest 参数

ES6 引入 rest 参数(形式为“…变量名”),用于获取函数的多余参数:

  1. function add(...values) {
  2. let sum = 0;
  3. for (var val of values) {
  4. sum += val;
  5. }
  6. return sum;
  7. }

扩展运算符

扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列:

  1. console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5
  2. [1, 2, ...more]
  3. [...arr1, ...arr2, arr3]
  4. [a, ...rest] = list
  5. [...'hello'] // [ "h", "e", "l", "l", "o" ];能够正确识别32位的Unicode字符;

基础用法1:展开

  1. const a = [2, 3, 4]
  2. const b = [1, ...a, 5]
  3. console.log(b);// [1, 2, 3, 4, 5]

基础用法2:收集

  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]

基础用法3:把类数组转换为数组

  1. const nodeList = document.getElementsByClassName("test");
  2. const array = [...nodeList];
  3. console.log(nodeList); // HTMLCollection [ div.test, div.test ]
  4. console.log(array); // Array [ div.test, div.test ]

使用 … 就可以实现类数组到数组的转换, 转换之后, 就可以使用数组的各种方法了。那么这个操作符出来之前是如何转换的呢?见下面例子:

  1. // es5
  2. function bar() {
  3. var args = Array.prototype.slice.call(arguments);
  4. args.push(4, 5, 6);
  5. return args;
  6. }
  7. console.log(bar(1, 2, 3)); // [1, 2, 3, 4, 5, 6]
  8. // es6
  9. function foo(...args) {
  10. args.push(4, 5, 6);
  11. return args;
  12. }
  13. console.log(foo(1, 2, 3)); // [1, 2, 3, 4, 5, 6]

基础用法4:为数组新增成员

  1. const peoples = ["jone", "jack"];
  2. const mrFan = "吴亦凡";
  3. const all = [...peoples, mrFan];
  4. console.log(all); // ["jone", "jack", "吴亦凡"]

基础用法5:为对象新增属性

  1. const obj = { name: 'jack', age: 30 }
  2. const result = { ...obj, sex: '男', height: '178cm' }
  3. console.log(result); // {name: "jack", age: 30, sex: "男", height: "178CM"}

基础用法6:合并数组或数组对象

  1. const a = [1, 2, 3];
  2. const b = [4, 5, 6];
  3. const result = [...a, ...b]; // [1, 2, 3, 4, 5, 6]

数组对象也一样

基础用法7:合并对象

  1. const people = {
  2. name: 'Lucy',
  3. age: 30,
  4. sex: '女'
  5. };
  6. const base = {
  7. age: 22,
  8. job: 'teacher',
  9. height: '168cm'
  10. }
  11. const all = { ...people, ...base };
  12. console.log(all); // {name: "Lucy", age: 22, sex: "女", job: "teacher", height: "168cm"}

注:相同的属性会覆盖掉

高级用法1:复制引用类型的数据

  1. const people = {
  2. name: 'Lucy',
  3. age: 30,
  4. sex: '女',
  5. hobbies: ['play games', 'basketball', 'swim']
  6. };
  7. const result = { ...people, ...people.hobbies };
  8. console.log(result); // {0: "play games", 1: "basketball", 2: "swim", name: "Lucy", age: 30, sex: "女", hobbies: Array(3)}

高级用法2:增加条件属性

例子1:

  1. const people = {
  2. name: 'Lucy',
  3. age: 30,
  4. sex: '女',
  5. hobbies: ['play games', 'basketball', 'swim']
  6. };
  7. const attrs = ['basketball', 'swim']
  8. const result = attrs ? { ...people, attrs } : people
  9. console.log(result); // {name: "Lucy", age: 30, sex: "女", hobbies: Array(3), attrs: Array(2)}

例子2:

  1. const people = {
  2. name: 'Lucy',
  3. age: 30,
  4. sex: '女',
  5. hobbies: ['play games', 'basketball', 'swim']
  6. };
  7. const attrs = ['basketball', 'swim']
  8. const result = {
  9. ...people,
  10. ...(attrs && { attrs })
  11. }
  12. console.log(result); // {name: "Lucy", age: 30, sex: "女", hobbies: Array(3), attrs: Array(2)}

类似于给对象添加属性

高级用法3:默认结构和添加默认属性

默认解构:我们知道, 当结构一个对象的时候, 如果这个对象里没有某个属性, 解出来是undefined , 我们可以添加默认值来解决:

  1. const people = {
  2. name: 'Lucy',
  3. age: 30,
  4. };
  5. let { name, age, sex = 'female' } = people;
  6. console.log(name, age, sex); // Lucy 30 female

箭头函数

  • 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误
  • 不可以使用 arguments 对象,该对象在函数体内不存在
  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
  1. function foo() {
  2. setTimeout(() => {
  3. console.log('id:', this.id);
  4. }, 100);
  5. }
  6. var id = 21;
  7. foo.call( { id: 42 } ); // id: 42
  8. function Timer () {
  9. this.s1 = 0;
  10. this.s2 = 0;
  11. setInterval(() => this.s1++, 1000); // 箭头函数
  12. setInterval(function () { // 普通函数
  13. this.s2++;
  14. }, 1000);
  15. }
  16. var timer = new Timer();
  17. setTimeout(() => console.log('s1: ', timer.s1), 3100); // s1: 3
  18. setTimeout(() => console.log('s2: ', timer.s2), 3100); // s2: 0
  19. // ES6 箭头函数 this 指针
  20. // ES6
  21. function foo() {
  22. setTimeout( () => {
  23. console.log('id:', this.id);
  24. },100);
  25. }
  26. // ES5
  27. function foo() {
  28. var _this = this;
  29. setTimeout(function () {
  30. console.log('id:', _this.id);
  31. }, 100);
  32. }

嵌套的箭头函数:

  1. const pipeline = (...funcs) =>
  2. val => funcs.reduce((a, b) => b(a), val);
  3. /*
  4. var pipeline = function(funcs) {
  5. return function (val) {
  6. // array.reduce(callback [, initialValue])
  7. // callback: function(previousValue, currentValue, currentIndex, array)
  8. return funcs.reduce(function(a, b) {
  9. return b(a);
  10. }, val);
  11. }
  12. }
  13. */
  14. const plus1 = a => a + 1;
  15. const mult2 = a => a * 2;
  16. const addThenMult = pipeline(plus1, mult2); // function
  17. addThenMult(5) // val -> 5; result: 12;

尾调用优化

  • 在 ES6 中,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
  • ES6的尾调用优化只在严格模式下开启,正常模式下是无效的。
  1. function f(x){
  2. let y = g(x); // 最后一步为赋值,所以不是尾调用
  3. return y;
  4. }
  5. function f(x){
  6. return g(x) + 1; // 也属于调用后还有操作,即使写在一行内,所以不是尾调用
  7. }
  8. function f(x){
  9. g(x); // 等价于: g(x); return undefined;
  10. }
  11. function f(x) {
  12. if (x > 0) {
  13. return m(x) // 不在最后一行,是最后一步,符合尾调用
  14. }
  15. return n(x);
  16. }

尾递归改写:

  1. function factorial(n) {
  2. if (n === 1) return 1;
  3. return n * factorial(n - 1);
  4. }
  5. // 改写为:
  6. function factorial(n, total = 1) {
  7. if (n === 1) return total;
  8. return factorial(n - 1, n * total);
  9. }
  10. // 或:
  11. function currying(fn, n) { // 柯里化
  12. return function (m) {
  13. return fn.call(this, m, n);
  14. };
  15. }
  16. function tailFactorial(n, total) {
  17. if (n === 1) return total;
  18. return tailFactorial(n - 1, n * total);
  19. }
  20. const factorial = currying(tailFactorial, 1);

对象扩展

ES6 允许直接写入变量和函数,作为对象的属性和方法:

  1. var foo = 'bar';
  2. var baz = {foo}; // baz = {foo: “bar"}
  3. //---
  4. function f(x, y) {
  5. return {x, y};
  6. }
  7. // 等同于
  8. function f(x, y) {
  9. return {x: x, y: y};
  10. }
  11. //---
  12. var o = {
  13. method() {
  14. return "Hello!";
  15. }
  16. };
  17. // 等同于
  18. var o = {
  19. method: function() {
  20. return "Hello!";
  21. }
  22. };
  23. var obj = {
  24. * m(){
  25. yield 'hello world';
  26. }
  27. }

应用场景一:

  1. class Point {
  2. constructor(x, y) {
  3. Object.assign(this, {x, y});
  4. }
  5. }
  6. Object.assign(SomeClass.prototype, {
  7. someMethod(arg1, arg2) {
  8. ···
  9. },
  10. anotherMethod() {
  11. ···
  12. }
  13. });
  14. function clone(origin) {
  15. return Object.assign({}, origin);
  16. }
  17. function clone(origin) {
  18. let originProto = Object.getPrototypeOf(origin);
  19. return Object.assign(Object.create(originProto), origin);
  20. }
  21. const merge =
  22. (target, ...sources) => Object.assign(target, sources);

应用场景二:

  1. const DEFAULTS = {
  2. logLevel: 0,
  3. outputFormat: 'html'
  4. };
  5. function processContent(options) {
  6. let options = Object.assign({}, DEFAULTS, options);
  7. }

Symbol

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。
JavaScript 语言的第七种数据类型:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)、Symbol

  1. var s1 = Symbol();
  2. var s2 = Symbol();
  3. s1 === s2 // false
  4. var s1 = Symbol("foo");
  5. var s2 = Symbol("foo");
  6. s1 === s2 // false
  7. var shapeType = {
  8. triangle: Symbol()
  9. };
  10. function getArea(shape, options) {
  11. var area = 0;
  12. switch (shape) {
  13. case shapeType.triangle:
  14. area = .5 * options.width * options.height;
  15. break;
  16. }
  17. return area;
  18. }
  19. //---
  20. var size = Symbol('size');
  21. class Collection {
  22. constructor() {
  23. this[size] = 0;
  24. }
  25. add(item) {
  26. this[this[size]] = item;
  27. this[size]++;
  28. }
  29. static sizeOf(instance) {
  30. return instance[size];
  31. }
  32. }

ES6 入门教程

https://es6.ruanyifeng.com/