块级作用域
ES6 中的 let 实际上才第一次引入了块级作用域这个概念,ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景:
var tmp = new Date();function f() {console.log(tmp);if (false) {var tmp = "hello world"; // 变量定义提升: var tmp; ... if (false) tmp = "hello world";}}f(); // undefined
变量泄露为全局变量:
var s = 'hello';for (var i = 0; i < s.length; i++) {console.log(s[i]);}console.log(i); // 5
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数:
// ES6严格模式'use strict';if (true) {function f() {} // ES5 会报错}// 不报错
自测,ES5 及 ES6 输出结果是什么?为什么?
function f() { console.log('I am outside!'); }(function () {if(false) {// 重复声明一次函数ffunction f() { console.log('I am inside!'); }}f();}());
由于 ES5 跟 ES6 对于块级作用域这个概念的差别,导致了 if 中的函数定义这一句作为一个语句块能否被外层读取,块级作用域是封闭的,最终输出的结果很好的体现了块级作用域这一概念。
反撇号跟模板字符串
反撇号`在键位 Esc 下方,能跟’和”一样普通地用于表示字符串,但相比之下多了可以向字符串中插入值的功能,代替 + 运算符的连接作用,反撇号括起来的字符串中可以以 ${变量名} 的形式去插入变量,这样的字符串叫做模板字符串。举个小例子。
var name = "豆浆";var age = 3;alert(`欢迎,${name}同学,你今年${age}岁了!`);
输出的结果自己体验吧~
${} 这个东西叫做占位符,它不仅可以放变量,还有函数或是算数运算等等都可以放进去,即 ${函数名},这样子就可以通过函数实时改变需要输出的信息,而且比起用 + 运算符来连接要更加简洁方便。后排提示,一定要注意使用英文的符号,不能用中文符号!
模板字符串不仅仅是用于代替连接作用,还有一些小特性。模板字符串在使用时可以互相嵌套,这种方法被称为模板套构,在模板字符串中如果再次出现反撇号`或是 $ 和 {} 等符号时跟”和’类似的,要使用转义字符 \。模板字符串插值实际上转换为字符串并输出的一个过程。
关于模板字符串的多行显示方式,支持 ES6 模板字符串的浏览器自然支持字符串不需要 \n 的换行方式,反撇号具备有’和”的基本功能,所以多行字符串的显示方式是相似的。
let/const
基本内容
- let: 声明变量
- const: 声明常量
- let/const: 块级作用域
- let命令、const命令声明的全局变量,不属于全局对象的属性
let 示例:
var a = [];for (let i = 0; i < 10; i++) {a[i] = function () {console.log(i);};}a[6](); // 6
var 示例:
var a = [];for (var i = 0; i < 10; i++) {a[i] = function () {console.log(i);};}a[6](); // 10
暂时性死区(TDZ)
只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。对于 let 不会有变量提升,但是在 let 所在的语句块会检测有无 let,如果调用 let 变量在声明前的话会报错,即后面所说的暂时性死区。
var tmp = 123;if (true) {tmp = 'abc'; // ReferenceErrorlet tmp;}
上面代码中,存在全局变量 tmp,但是块级作用域内 let 又声明了一个局部变量 tmp,导致后者绑定这个块级作用域,所以在 let 声明变量前,对 tmp 赋值会报错。在同一语句块中不能对同一 let 变量,用 let 又用 var 定义,函数的形参也不能在函数体内定义为 let,默认已经定义好了,再用 let 也会变为重复声明,同一语句块中同名变量不能反复声明。
ES6 明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为’’’“暂时性死区”(temporal dead zone,简称TDZ)’’’。
if (true) {// TDZ开始tmp = 'abc'; // ReferenceErrorconsole.log(tmp); // ReferenceErrorlet tmp; // TDZ结束; 若使用 var 那么由于变量定义提升,开始的 tmp 不会报错console.log(tmp); // undefinedtmp = 123;console.log(tmp); // 123}
相同作用域内不能重复声明
// 报错function () {let a = 10;var a = 1;}// 报错function () {let a = 10;let a = 1;}
因此,不能在函数内部重新声明参数。
function func(arg) {let arg; // 报错}function func(arg) {{let arg; // 不报错}}
第二段代码不报错是因为新创建了一段语句块,新的 arg 与形参 arg 不在同一语句块中,没有冲突。
变量的解构赋值
数组
基本形式:
let [foo, [[bar], baz]] = [1, [[2], 3]];foo // 1bar // 2baz // 3let [ , , third] = ["foo", "bar", "baz"];third // "baz"let [x, , y] = [1, 2, 3];x // 1y // 3let [head, ...tail] = [1, 2, 3, 4];head // 1tail // [2, 3, 4]let [x, y, ...z] = ['a'];x // "a"y // undefinedz // []
只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值:
function* fibs() {var a = 0;var b = 1;while (true) {yield a;[a, b] = [b, a + b];}}var [first, second, third, fourth, fifth, sixth] = fibs();sixth // 5
解构赋值允许指定默认值,ES6 内部使用严格相等运算符(===),判断一个位置是否有值:
var [foo = true] = [];foo // truevar [x = 1] = [undefined];x // 1var [x = 1] = [null];x // null
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值:
function f() {console.log('aaa');}let [x = f()] = [1];// 等价于:let x;if ([1][0] === undefined) {x = f();} else {x = [1][0];}
对象的解构赋值
示例:
var { bar, foo } = { foo: "aaa", bar: "bbb" };foo // "aaa"bar // "bbb"//---var { foo: baz } = { foo: "aaa", bar: "bbb" };baz // "aaa"foo // error: foo is not defined//---var obj = {p: ["Hello",{ y: "World" }]};var { p: [x, { y }] } = obj;x // "Hello"y // "World"// 解构赋值时,如果等号右边是数值和布尔值,则会先转为对象let {toString: s} = 123;s === Number.prototype.toString // true//函数的参数也可以使用解构赋值[[1, 2], [3, 4]].map(([a, b]) => a + b);var map = new Map();for (let [key, value] of map) {console.log(key + " is " + value);}const { SourceMapConsumer, SourceNode } = require("source-map");
数组扩展
Array.from
Array.from 方法用于将两类对象转为真正的数组:类似数组(必须有 length 属性)的对象(array-like object)和可遍历(iterable)的对象:
function foo() {var args = Array.from(arguments);// ...}Array.from({ length: 3 }); // [undefined, undefined, undefined]
扩展运算符(…)也可以将某些数据结构转为数组;扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换:
function foo() {var args = [...arguments];}
Array.from 还可以接受第二个参数,作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组:
Array.from(arrayLike, x => x * x); // Array.from(arrayLike).map(x => x * x);
Array.of
将一组值,转换为数组:
Array.of(3, 11, 8) // [3,11,8]
find/findIndex
[1, 4, -5, 10].find((n) => n < 0)[1, 5, 10, 15].find(function(value, index, arr) {return value > 9;}) // 10[1, 5, 10, 15].findIndex(function(value, index, arr) {return value > 9;}) // 2
函数扩展
默认参数
function Point(x = 0, y = 0) {this.x = x;this.y = y;}function fetch(url, { body = '', method = 'GET', headers = {} }){console.log(method);}(function (a, b, c = 5) {}).length // 2 -> 函数的length属性,将返回没有指定默认值的参数个数
如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域:
var x = 1;function f(x, y = x) {console.log(y);}f(2); // 2let foo = 'outer';function bar(func = x => foo) {let foo = 'inner';console.log(func()); // outer}bar();// 参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中// 的函数就不会运行)function throwIfMissing() {throw new Error('Missing parameter');}function foo(mustBeProvided = throwIfMissing()) {return mustBeProvided;}foo();
rest 参数
ES6 引入 rest 参数(形式为“…变量名”),用于获取函数的多余参数:
function add(...values) {let sum = 0;for (var val of values) {sum += val;}return sum;}
扩展运算符
扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列:
console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5[1, 2, ...more][...arr1, ...arr2, …arr3][a, ...rest] = list[...'hello'] // [ "h", "e", "l", "l", "o" ];能够正确识别32位的Unicode字符;
基础用法1:展开
const a = [2, 3, 4]const b = [1, ...a, 5]console.log(b);// [1, 2, 3, 4, 5]
基础用法2:收集
function foo(a, b, ...c) {console.log(a, b, c)}foo(1, 2, 3, 4, 5); // 1, 2, [3, 4, 5]
基础用法3:把类数组转换为数组
const nodeList = document.getElementsByClassName("test");const array = [...nodeList];console.log(nodeList); // HTMLCollection [ div.test, div.test ]console.log(array); // Array [ div.test, div.test ]
使用 … 就可以实现类数组到数组的转换, 转换之后, 就可以使用数组的各种方法了。那么这个操作符出来之前是如何转换的呢?见下面例子:
// es5function bar() {var args = Array.prototype.slice.call(arguments);args.push(4, 5, 6);return args;}console.log(bar(1, 2, 3)); // [1, 2, 3, 4, 5, 6]// es6function foo(...args) {args.push(4, 5, 6);return args;}console.log(foo(1, 2, 3)); // [1, 2, 3, 4, 5, 6]
基础用法4:为数组新增成员
const peoples = ["jone", "jack"];const mrFan = "吴亦凡";const all = [...peoples, mrFan];console.log(all); // ["jone", "jack", "吴亦凡"]
基础用法5:为对象新增属性
const obj = { name: 'jack', age: 30 }const result = { ...obj, sex: '男', height: '178cm' }console.log(result); // {name: "jack", age: 30, sex: "男", height: "178CM"}
基础用法6:合并数组或数组对象
const a = [1, 2, 3];const b = [4, 5, 6];const result = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
基础用法7:合并对象
const people = {name: 'Lucy',age: 30,sex: '女'};const base = {age: 22,job: 'teacher',height: '168cm'}const all = { ...people, ...base };console.log(all); // {name: "Lucy", age: 22, sex: "女", job: "teacher", height: "168cm"}
高级用法1:复制引用类型的数据
const people = {name: 'Lucy',age: 30,sex: '女',hobbies: ['play games', 'basketball', 'swim']};const result = { ...people, ...people.hobbies };console.log(result); // {0: "play games", 1: "basketball", 2: "swim", name: "Lucy", age: 30, sex: "女", hobbies: Array(3)}
高级用法2:增加条件属性
例子1:
const people = {name: 'Lucy',age: 30,sex: '女',hobbies: ['play games', 'basketball', 'swim']};const attrs = ['basketball', 'swim']const result = attrs ? { ...people, attrs } : peopleconsole.log(result); // {name: "Lucy", age: 30, sex: "女", hobbies: Array(3), attrs: Array(2)}
例子2:
const people = {name: 'Lucy',age: 30,sex: '女',hobbies: ['play games', 'basketball', 'swim']};const attrs = ['basketball', 'swim']const result = {...people,...(attrs && { attrs })}console.log(result); // {name: "Lucy", age: 30, sex: "女", hobbies: Array(3), attrs: Array(2)}
高级用法3:默认结构和添加默认属性
默认解构:我们知道, 当结构一个对象的时候, 如果这个对象里没有某个属性, 解出来是undefined , 我们可以添加默认值来解决:
const people = {name: 'Lucy',age: 30,};let { name, age, sex = 'female' } = people;console.log(name, age, sex); // Lucy 30 female
箭头函数
- 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
- 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误
- 不可以使用 arguments 对象,该对象在函数体内不存在
- 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
function foo() {setTimeout(() => {console.log('id:', this.id);}, 100);}var id = 21;foo.call( { id: 42 } ); // id: 42function Timer () {this.s1 = 0;this.s2 = 0;setInterval(() => this.s1++, 1000); // 箭头函数setInterval(function () { // 普通函数this.s2++;}, 1000);}var timer = new Timer();setTimeout(() => console.log('s1: ', timer.s1), 3100); // s1: 3setTimeout(() => console.log('s2: ', timer.s2), 3100); // s2: 0// ES6 箭头函数 this 指针// ES6function foo() {setTimeout( () => {console.log('id:', this.id);},100);}// ES5function foo() {var _this = this;setTimeout(function () {console.log('id:', _this.id);}, 100);}
嵌套的箭头函数:
const pipeline = (...funcs) =>val => funcs.reduce((a, b) => b(a), val);/*var pipeline = function(funcs) {return function (val) {// array.reduce(callback [, initialValue])// callback: function(previousValue, currentValue, currentIndex, array)return funcs.reduce(function(a, b) {return b(a);}, val);}}*/const plus1 = a => a + 1;const mult2 = a => a * 2;const addThenMult = pipeline(plus1, mult2); // functionaddThenMult(5) // val -> 5; result: 12;
尾调用优化
- 在 ES6 中,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
- ES6的尾调用优化只在严格模式下开启,正常模式下是无效的。
function f(x){let y = g(x); // 最后一步为赋值,所以不是尾调用return y;}function f(x){return g(x) + 1; // 也属于调用后还有操作,即使写在一行内,所以不是尾调用}function f(x){g(x); // 等价于: g(x); return undefined;}function f(x) {if (x > 0) {return m(x) // 不在最后一行,是最后一步,符合尾调用}return n(x);}
尾递归改写:
function factorial(n) {if (n === 1) return 1;return n * factorial(n - 1);}// 改写为:function factorial(n, total = 1) {if (n === 1) return total;return factorial(n - 1, n * total);}// 或:function currying(fn, n) { // 柯里化return function (m) {return fn.call(this, m, n);};}function tailFactorial(n, total) {if (n === 1) return total;return tailFactorial(n - 1, n * total);}const factorial = currying(tailFactorial, 1);
对象扩展
ES6 允许直接写入变量和函数,作为对象的属性和方法:
var foo = 'bar';var baz = {foo}; // baz = {foo: “bar"}//---function f(x, y) {return {x, y};}// 等同于function f(x, y) {return {x: x, y: y};}//---var o = {method() {return "Hello!";}};// 等同于var o = {method: function() {return "Hello!";}};var obj = {* m(){yield 'hello world';}}
应用场景一:
class Point {constructor(x, y) {Object.assign(this, {x, y});}}Object.assign(SomeClass.prototype, {someMethod(arg1, arg2) {···},anotherMethod() {···}});function clone(origin) {return Object.assign({}, origin);}function clone(origin) {let originProto = Object.getPrototypeOf(origin);return Object.assign(Object.create(originProto), origin);}const merge =(target, ...sources) => Object.assign(target, …sources);
应用场景二:
const DEFAULTS = {logLevel: 0,outputFormat: 'html'};function processContent(options) {let options = Object.assign({}, DEFAULTS, options);}
Symbol
ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。
JavaScript 语言的第七种数据类型:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)、Symbol
var s1 = Symbol();var s2 = Symbol();s1 === s2 // falsevar s1 = Symbol("foo");var s2 = Symbol("foo");s1 === s2 // falsevar shapeType = {triangle: Symbol()};function getArea(shape, options) {var area = 0;switch (shape) {case shapeType.triangle:area = .5 * options.width * options.height;break;}return area;}//---var size = Symbol('size');class Collection {constructor() {this[size] = 0;}add(item) {this[this[size]] = item;this[size]++;}static sizeOf(instance) {return instance[size];}}
