画布 1
Javascript
  对象
    六种主要类型
      string
      number
      boolean
      null
      undefined
      object
    属性描述符
      writable
      enumerable
      configurable
      value
    遍历
      some
      every
      forEach
      for ... in
      for ... of
    对象拷贝
      深拷贝
        JSON.parse(JSON.stringify())
        递归复制所有层级属性
        $.extend
      浅拷贝
      Object.assign()仅对最外一层做了深拷贝，里面的对象仍然是浅拷贝
      Object.assign()仅对最外一层做了深拷贝，里面的对象仍然是浅拷贝
  var
    其创建的全局变量无法删除（delete）
    无var创建的隐式全局变量可删除（delete）
  花括号的位置
    分号插入机制(semicolon insertion mechanism)
  原型链
    原型 prototype
  作用域
    异常
      ReferenceError
      TypeError
    函数作用域
      匿名与具名
      立即执行函数 IIFE
        解决 undefined 标识符的默认值被错误覆盖导致的异常
        倒置代码的运行顺序
    块作用域
      垃圾收集
  函数
    函数参数
      所有函数的参数都是按值传递的
    命名函数表达式
      声明
      表达式
    隐式转换
      当我们没有重新定义toString与valueOf时，函数的隐式转换会调用默认的toString方法，它会将函数的定义内容作为字符串返回。而当我们主动定义了toString/vauleOf方法时，那么隐式转换的返回结果则由我们自己控制了。其中valueOf的优先级会toString高一点。
    函数式编程
      柯里化
        多参数的函数转换成单参数的形式
  变量
    NaN
    变量声明被提前（作用域范围内）
    null 与 undefined
  闭包
    用处
      读取函数内部变量
      让这些变量始终保持在内存中
    注意
      由于闭包会使得函数中的变量都被保存在内存中，内存消耗很大，
所以不能滥用闭包，否则会造成网页的性能问题，在IE中可能导致内存泄露。
解决方法是，在退出函数之前，将不使用的局部变量全部删除。
      闭包会在父函数外部，改变父函数内部变量的值。所以，如果你把父函数当作对象（object）使用，把闭包当作它的公用方法（Public Method），把内部变量当作它的私有属性（private value），这时一定要小心，不要随便改变父函数内部变量的值。
  改变环境上下文
    apply/call
      apply
      call
      bind
      绑定后立即执行
      绑定后非立即执行
  声明提升
    编译器执行流程
      1. 编译阶段，定义声明
      2. 执行阶段，保留原地
    函数及变量声明都会被提升
      函数表达式不会被提升
      使用let定义的变量不会被提升
    函数声明优先被提升
    被条件判断所控制的函数声明
  模块化规范
    CommonJS
      缺陷
        没有模块系统
        标准库较少
        没有标准接口
        缺乏包管理系统
    AMD(依赖前置)
    CMD(依赖就近)
    为兼容多种规范，可以将类库代码包装在一个闭包中
  this
    绑定规则
      默认绑定
      隐式绑定
      显式绑定
      new绑定
      优先级
        1. 函数中是否使用new调用，若是则this绑定的是新创建的对象
        2. 函数是否通过call、apply或硬绑定调用，若是则this绑定的是指定的对象
        3. 函数是否在某个上下文对象中调用，若是则this绑定的是该上下文对象
        4. 以下都是则使用默认绑定
        特例
          1. 将null或undefined作为绑定对象传入call,apply或者bind，实际应用的是默认绑定
          间接引用函数会应用默认绑定规则
  Event loop(事件循环)
    同步任务
    异步任务
      微观任务
        Promise
        process.nextTick
        MutationObserver
      宏观任务
        setTimeout
        setInterval
        setImmediate
        UI rendering
        I/O
  ECMAScript
    版本
      ES 6
        函数扩展
          箭头函数 =>
            函数体内的this对象是定义时所在的对象，而不是使用时所在的对象
            不可当做构造函数，即不可以使用new命令，否则报错
            不可以使用arguments对象，该对象在函数体内不存在。
如果要用，可以用 rest 参数代替。
            不可以使用yield命令，因此箭头函数不能用作 Generator 函数
            无法使用apply, class， bind
          参数默认值
          rest参数
          函数内部严格模式
          name属性
          :: 双冒号运算符
          尾调用优化
            尾调用
            尾递归
              递归函数的改写
                斐波那契数 Fibonacci
                  一般实现，复杂度为O(N*lgN)
                  优化后的实现
                  引申其他非递归实现：
                    时间复杂度为O(n)，空间复杂度为O(1)
                    时间复杂度为O(n)，空间复杂度为O(n)
                阶乘
                简单累加，每次加一
                  一般实现
                  优化后
          函数参数尾逗号
        let与const
          let
            仅在代码块内有效
            不存在变量提升的行为
            暂时性死区
              特例
            不允许在相同作用域内重复声明
            引申for作用域，即就是设置循环变量的那部分是一个父作用域，
而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i); // 输出三次abc
}
因为如果在同一个代码块中使用let同时定义相同的两个变量时会提示错误：
Uncaught SyntaxError: Identifier 'a' has already been declared
        类
        模块
        Promise
        模板字符串
        解构赋值
      ES 7
        Array.prototype.includes
        指数操作符 **
      ES 8
        async/await
        Object.values()
        Object.entries()
        String padding
        Object.getOwnPropertyDescriptors()
        函数参数列表结尾允许逗号
      ES 9
        异步迭代
        Rest/Spread 属性
        正则表达式命名捕获组
        正则表达式反向断言
      ES 10
    规范提交流程
      stage0: 想法提交阶段
      stage1：提案阶段
      stage2：草案阶段
      stage3：候选阶段
      stage4：完成阶段
  书籍
    你不知道的Javascript（上中下）
    JavaScript高级程序设计
typeof(null) === object
for...of是es6引入的新遍历方式，使用的是迭代器，其原理相当于：
var myObject = {
a: 2,
b: 3
};
Object.defineProperty( myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function() {
var o = this;
var idx = 0;
var ks = Object.keys( o );
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.length)
};
}
};
}
} );
// 手动遍历 myObject
var it = myObject[Symbol.iterator]();
var x = it.next();
while(!x.done){
console.log(x.value);;
x = it.next();
}
'use strict';
let obj1 = { a: 0 , b: { c: 0, d:{ee: 323 }}};
let obj2 = Object.assign({}, obj1);
console.log(JSON.stringify(obj2));
// 深拷贝
obj1.a = 1;
console.log(JSON.stringify(obj1));
console.log(JSON.stringify(obj2));
// 深拷贝
obj2.a = 2;
console.log(JSON.stringify(obj1));
console.log(JSON.stringify(obj2));
// 以下两个例子标明除了最外一层，里面的对象都是浅拷贝
obj1.b.c = 3;
console.log(JSON.stringify(obj1));
console.log(JSON.stringify(obj2));
obj2.b.c = 5;
console.log(JSON.stringify(obj1));
console.log(JSON.stringify(obj2));
obj1.b.d.ee = 988;
console.log(JSON.stringify(obj1));
console.log(JSON.stringify(obj2));
obj2.b.d.ee = 123;
console.log(JSON.stringify(obj1));
console.log(JSON.stringify(obj2));
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
此时constructor 属性不再指向 Person,而是指向object :
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true
若构造函数很重要则可写成 :
( 此时constructor 的 Enumerable 会被设置为true )
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
默认情况下，原生的 constructor 属性是不可枚举的，因此如果你使用兼容 ECMAScript 5 的 JavaScript 引擎，可以试一试Object.defineProperty() :
//重设构造函数，只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
function foo(a) {
var b;
console.log( a + b );
b = a;
}
foo( 2 );
这是一个“未声明”的变量，因为在任何相关的作用域中都无法找到它。在所有嵌套的作用域中遍寻不到所需的变量，引擎就会抛出 ReferenceError 异常
在作用域中找不到相应变量就会提示该错误
变量已声明，但未赋值，导致执行相应操作报错，
如 var a ; a(12);
变量a仅做了声明，但是里面被当做函数来执行
将一个参数命名为 undefined，但是在对应的位置不传入任何值，这样就
可以保证在代码块中 undefined 标识符的值真的是 undefined：
undefined = true; // 给其他代码挖了一个大坑！绝对不要这样做！
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
var a = 2;30
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
function process(data) {
// 在这里做点有趣的事情
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase=*/false );
click 函数的点击回调并不需要 someReallyBigData 变量。理论上这意味着当 process(..) 执
行后，在内存中占用大量空间的数据结构就可以被垃圾回收了。但是，由于 click 函数形成
了一个覆盖整个作用域的闭包， JavaScript 引擎极有可能依然保存着这个结构（取决于具体
实现）。块作用域可以打消这种顾虑，可以让引擎清楚地知道没有必要继续保存 someReallyBigData 了：
function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容完事可以销毁！
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
Example:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
参数按值传递，执行setName函数时会将person所指向的Object实例地址复制一份，传到参数内部。该副本及其person所指向的都是同一个内存区域，即他们存储的指针地址都一样。
栗子1：
function fn() {
return 20;
}
console.log(fn + 10);
栗子2：
function fn() {
return 20;
}
fn.toString = function() {
return 10;
}
console.log(fn + 10); // 输出结果是多少？
栗子3：
function fn() {
return 20;
}
fn.toString = function() {
return 10;
}
fn.valueOf = function() {
return 5;
}
console.log(fn + 10);
function currying(fn, n) {
return function (m) {
return fn.call(this, m, n);
};
}
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
alert(isNaN(true)); //false（可以被转换成数值 1）
1. null 不是空对象指针，而是基本数据类型，故有：
typeof(null) // object
在 JavaScript 中二进制前三位都为 0 的话会被判
断为 object 类型， null 的二进制表示是全 0，自然前三位也是 0，所以执行 typeof 时会返回“object”
2. undefined 派生自 null
console.log(null == undefined); //true
console.log(null === undefined); //false
闭包就是能够读取其他函数内部变量的函数。
栗子1：
getNameFunc返回的匿名函数的执行环境是全局的，而且this只在函数内部起作用。此时的this.name在匿名函数中找不到，所以就从全局中找，找到后打印出来。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
栗子2：
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
栗子1：
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
原因就在于f1是f2的父函数，而f2被赋给了一个全局变量，这导致f2始终在内存中，而f2的存在依赖于f1，因此f1也始终在内存中，不会在调用结束后，被垃圾回收机制（garbage collection）回收。
栗子2:
for( var i = 0; i < 5; i++ ) {
setTimeout(() => {
console.log( i );
}, 1000 * i)
}
// 结果为5个5，每秒输出一个
根据作用域的工作原理，实际情况是尽管循环中的五个函数是在各个迭代中分别定义的，
但是它们都被封闭在一个共享的全局作用域中，因此实际上只有一个 i。
栗子3：
for (var i = 1; i <= 5; i++) {
let j = i; // 输出结果正常
setTimeout(function timer() {
console.log(j);
},j*1000);
}
栗子4：
for (var i = 1; i <= 5; i++) {
var j = i; // 输出结果仍然是5个5
setTimeout(function timer() {
console.log(j);
},j*1000);
}
栗子5：
for( var i = 0; i < 5; i++ ) {
((j) => {
setTimeout(() => {
console.log( j );
}, 1000 * j)
})(i)
}
栗子6：
function test(){
for (let i=0; i<5; i++) { // 正常
setTimeout( function timer() {
console.log(new Date(),i);
}, i*1000 );
}
// console.log("end",new Date(),i); //因为变量作用域的问题，这里会报i 不存在，未声明
}
都是为了改变函数执行上下文
1. 改变this指针，第一个参数都是this要指向的对象，也就是想指定的上下文；
2. 借用其他对象的方法
下面就借用一道面试题，来更深入的去理解下 apply 和 call 。
定义一个 log 方法，让它可以代理 console.log 方法，常见的解决方法是：
function log(msg)　{
console.log(msg);
}
log(1); //1
log(1,2); //1
上面方法可以解决最基本的需求，但是当传入参数的个数是不确定的时候，上面的方法就失效了，这个时候就可以考虑使用 apply 或者 call，注意这里传入多少个参数是不确定的，所以使用apply是最好的，方法如下：
function log(){
console.log.apply(console, arguments);
};
log(1); //1
log(1,2); //1 2
接下来的要求是给每一个 log 消息添加一个"(app)"的前辍，比如：
log("hello world"); //(app)hello world
该怎么做比较优雅呢?这个时候需要想到arguments参数是个伪数组，通过 Array.prototype.slice.call 转化为标准数组，再使用数组方法unshift，像这样：
function log(){
var args = Array.prototype.slice.call(arguments);
args.unshift('(app)');
console.log.apply(console, args);
};
func.apply(this, [arg1, arg2])
func.call(this, arg1, arg2);
绑定后返回一个函数，待后续调用。
多次 bind() 是无效的，更深层次的原因， bind() 的实现，相当于使用函数在内部包了一个 call / apply ，第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
函数声明会被提升，但是函数表达式却不会被提升。
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
函数声明会被提升，但是函数表达式却不会被提升。
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
即使是具名的函数表达式，名称标识符在赋值之前也无法在所在作用域中使用：
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
会输出 1 而不是 2 ！这个代码片段会被引擎理解为如下形式：
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};
注意， var foo 尽管出现在 function foo()... 的声明之前，但它是重复的声明（因此被忽
略了），因为函数声明会被提升到普通变量之前。
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
}
以同步方式加载模块，主要用在后端。
var clock = require('clock');
clock.start();
这种写法适合服务端，因为在服务器读取模块都是在本地磁盘，加载速度很快。但是如果在客户端，加载模块的时候有可能出现“假死”状况。
异步的加载模块，Asynchronous Module Definition：
require(['clock'],function(clock){
clock.start();
});
AMD虽然实现了异步加载，但是开始就把所有依赖写出来是不符合书写的逻辑顺序的，能不能像commonJS那样用的时候再require，而且还支持异步加载后再执行呢？所以才有了CMD
由国内的玉伯提出，CMD (Common Module Definition), 是seajs推崇的规范，CMD则是依赖就近，用的时候再require。它写起来是这样的：
define(function(require, exports, module) {
var clock = require('clock');
clock.start();
});
AMD和CMD最大的区别是对依赖模块的执行时机处理不同，而不是加载的时机或者方式不同，二者皆为异步加载模块。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隐式绑定丢失，退回默认板顶的栗子1：
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名！
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
栗子2：
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置！
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
栗子3：
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
function foo() {
console.log( this.a );
}98 ｜ 第 2 章
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
Ecmascript 2015
箭头函数转成 ES5 的代码如下。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
// ES6 箭头函数
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5 中对应的写法为
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
栗子1：
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 }); // id: 42
上面代码中，setTimeout的参数是一个箭头函数，这个箭头函数的定义生效是在foo函数生成时，而它的真正执行要等到 100 毫秒后。如果是普通函数，执行时this应该指向全局对象window，这时应该输出21。但是，箭头函数导致this总是指向函数定义生效时所在的对象（本例是{id: 42}），所以输出的是42。
栗子2： 下面有几个this对象
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
this指向的固定化，并不是因为箭头函数内部有绑定this的机制，实际原因是箭头函数根本没有自己的this，导致内部的this就是外层代码块的this。正是因为它没有this，所以也就不能用作构造函数。
rest参数之后不能再有其他参数，否则报错
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
双冒号左边是一个对象，右边是一个函数。该运算符会自动将左边的对象，作为上下文环境（即this对象），绑定到右边的函数上面。
如果双冒号左边为空，右边是一个对象的方法，则等于将该方法绑定在该对象上面。
即减少调用栈，防止栈溢出stackoverflow
即某个函数的最后一步是调用另一个函数。
尾调用由于是函数的最后一步操作，所以不需要保留外层函数的调用帧，因为调用位置、内部变量等信息都不会再用到了，只要直接用内层函数的调用帧，取代外层函数的调用帧就可以了。
函数调用自身，称为递归。如果尾调用自身，就称为尾递归。
非尾递归的 Fibonacci 数列实现如下。
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
尾递归优化过的 Fibonacci 数列实现如下。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
function fibonacci(n){
var a,b,res;
a = b = 1;
if(n == 0){
return 0;
}
if(n == 1 || n == 2){
return 1;
}
for(var i=2;i<n;i++){
res = a + b;
a = b;
b = res;
}
return res;
}
function fibo(x){
var arr = new Array(x);
arr[0] = 1;
arr[1] = 1;
for(var index=2;index<x;index++){
arr[index] = arr[index-1]+arr[index-2];
}
var result = 0;
arr.forEach((item)=>{
result+=item;
});
return result;
}
fibo(5);
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
function factorial(n) {
return tailFactorial(n, 1);
}
factorial(5) // 120
function sum(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}
sum(1, 100000)
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)
栗子1：
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i);
// ReferenceError: i is not defined
栗子2：
尽管循环中的匿名函数是在各个迭代中分别定义的，
但是它们都被封闭在一个共享的全局作用域中，因此实际上只有一个 i。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
如果使用let，声明的变量仅在块级作用域内有效，最后输出的是 6。因为每一次循环的i其实都是一个新的变量，这是因为 JavaScript 引擎内部会记住上一轮循环的值，初始化本轮的变量i时，就在上一轮循环的基础上进行计算。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
即变量未声明前无法对其进行操作。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
作为比较，如果一个变量根本没有被声明，使用typeof反而不会报错。
typeof undeclared_variable // "undefined"
Ecmascript 2016
Ecmascript 2017
Object.values()是一个与Object.keys()类似的新函数，但返回的是Object自身属性的所有值，不包括继承的值。
for(let [key,value] of Object.entries(obj1)){
console.log(`key: ${key} value:${value}`)
}
在ES8中String新增了两个实例函数String.prototype.padStart和String.prototype.padEnd，允许将空字符串或其他字符串添加到原始字符串的开头或结尾
const reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
match = reDate.exec('2018-04-30');
year = match.groups.year;
month = match.groups.month;
day = match.groups.day;