你不知道的JS系列——ES6 & Beyond

956 阅读23分钟

人生最遗憾的,莫过于:轻易地放弃了不该放弃的,固执地坚持了不该坚持的

写在前面

学习总结 + 个人理解,巩固 + 方便查阅,大家愿意看的简单看看就好

注:这里只总结 ES6 中个人不常用但又比较重要的一些知识点及ES2020的新知识。

块级作用域

一道有意思的题目:

// 打印结果为在 chrome 控制台下的
{
    function a(){};
    a = 50;
}
console.log(a); // 打印:ƒ a(){}
{
    b = 50;
    function b(){};
}
console.log(b); // 打印:50
// 严格模式下
'use strict'
var d; 
{
    function d(){};
    d = 50;
}
console.log(d); // 打印:undefined

这道题,怎么想结果都是出乎意料的,但大可不必为此头疼,下面解释:

ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

注意: 上面的一个词类似,体现在函数声明在块外无法被访问,但在块内会被"提升",与 let 声明相反,后者会遇到 TDZ(暂时性死区) 错误陷阱。
实 🌰 见分晓:

// 标准的 ES6 中
{
    foo(); // 可以这么做!
    function foo() {
        // ..
    }
}
foo(); // ReferenceError

上面所说是在标准的 ES6 中,但如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式:

  • 允许在块级作用域内声明函数
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部
  • 同时,函数声明还会提升到所在的块级作用域的头部

所以上面恼人的题目结果就是浏览器自己实现方式导致的差异性,研究这些并没有任何实际意义。平时声明块级作用域,用let、const声明,对于变量先声明再使用,代码规范化并不会出现这些问题,所以不要庸人自扰。如果你还是执拗于问题本身的答案(反正我是不感兴趣),那你不妨去看看这篇文章: 《研究js的块级作用域中的变量声明和函数声明》

顺便一提: JS 普遍被认为是动态语言,即解释型语言,但事实上它是一门编译语言,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。简单地说,任何JavaScript代码片段在执行前都要进行编译(通常就在执行前)。引擎会在解释JavaScript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理,所以提升是在编译时出现的。每个作用域都会进行提升操作,函数声明优先于变量声明被提升,要特别注意的一点是,即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用:

foo(); // TypeError 注意错误类型,这里指这不是一个函数
bar(); // ReferenceError 这里指未定义,无法使用
var foo = function bar() {
    // ...
};
// 经提升后实际的形式
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
    var bar = ...self...
    // ...
}

默认参数值

  • JavaScrpt 设计原则:undefined 意味着缺失
  • 默认参数值表达式是惰性求值
  • 函数声明中形式参数是在它们自己的作用域中(可以把它看作是就在函数声明包裹的 ( .. ) 的作用域中),而不是在函数体作用域中。即在默认值表达式中的标识符引用首先匹配到形式参数作用域,然后才会搜索外层作用域

实 🌰 见分晓:

var w = 1, z = 2;
function foo( x = w + 1, y = x + 1, z = z + 1 ) {
    console.log( x, y, z );
}
foo(); // TDZ ReferenceError

解析:w + 1 默认值表达式中的 w 先在形式参数列表作用域中寻找 w,但是没有找到,所以就使用外层作用域的 wx + 1 默认值表达式中的 x 在形式参数列表作用域中寻找 x,很幸运这里 x 已经初始化。z + 1 默认值表达式中的 z是一个此刻还没初始化的参数变量,所以它永远不会试图从外层作用域寻找 z

解构赋值

可以理解为结构化赋值,专用于数组和对象。

不只是声明

赋值表达式并不必须是变量标识符,任何合法的赋值表达式都可以:

var o = {};
[o.a, o.b, o.c] = [1, 2, 3];
( { x: o.x, y: o.y, z: o.z } =  { x: 4,  y: 5, z: 6 } );
console.log( o.a, o.b, o.c ); // 1 2 3
console.log( o.x, o.y, o.z ); // 4 5 6

在解构中使用计算出的属性表达式。考虑:

var which = "x",
 o = {};
( { [which]: o[which] } = { x: 4,  y: 5, z: 6 } );
console.log( o.x ); // 4

重复赋值

对象解构形式允许多次列出同一个源属性(持有值类型任意)。
实 🌰 见分晓:

var { a: X, a: Y } = { a: 1 };
X; // 1
Y; // 1

var { a: { x: X, x: Y }, a } = { a: { x: 1 } };
X; // 1
Y; // 1
a; // { x: 1 } 

默认值赋值

解构的两种形式都可以提供一个用来赋值的默认值:

var { x = 5, y = 10, z = 15, w = 20 } = { x: 4,  y: 5, z: 6 };
console.log( x, y, z, w ); // 4 5 6 20

var { x, y, z, w: WW = 20 } = { x: 4,  y: 5, z: 6 };
console.log( x, y, z, WW ); // 4 5 6 20

对象字面量扩展

简洁方法

注意: 执行递归或者事件绑定 / 解绑定的时候,不适用对象中方法的简洁写法,箭头函数也不适用。因为简洁写法与箭头函数都是匿名函数,严格模式下arguments.callee、arguments.caller也无法访问。
实 🌰 见分晓:

 var foo = {
    // 这里不适用简洁写法或箭头函数,因为下面有递归使用
    something: function something(x,y) { 
        if (x > y) {
            // 交换x和y的递归调用
            return something( y, x ); 
            // 如果写this.something( y, x ),注意this指向可能丢失的情况
        }
        return y - x;
    }
}

关于super:通常把 super 看作只与类相关。但是,鉴于 JavaScript 的原型类而非类对象的本质,super 对于普通对象的简洁方法也一样有效,特性也基本相同。

注意: super 只允许在简洁方法中出现,而不允许在普通函数表达式属性中出现。也只允许以 super.XXX 的形式(用于属性 / 方法访问)出现,而不能以 super() 的形式出现。

标签模板字面量

对于模板字符串,个人感觉很像JSP页面中的el表达式。模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串,这被称为“标签模板”功能。标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

这里的注意点是参数,举 🌰 :

function foo(strings, ...values) {
    console.log( strings );
    console.log( values );
}
var desc = "awesome";
foo`Everything is ${desc}`;
// [ "Everything is ", ""]  着重注意这里的空字符串
// [ "awesome" ]

实际应用场景(把数字格式化为美元表示法):

function dollabillsyall(strings, ...values) {
    return strings.reduce( function(s,v,idx){ 
    // 注意idx从 1 开始,因为未给reduce设置初始值参数
        if (typeof values[idx-1] == "number") {
            s += `$${values[idx-1].toFixed( 2 )}`;
        }
        else {
            s += values[idx-1];
        }
        return s + v;
    });
}
var amt1 = 12.00,
    name = "Kyle";
console.log( 
    dollabillsyall
    `Thanks for your purchase, ${name}! 
    Your product cost was ${amt1}, 
    which with tax comes out to ${amt1 * 1.20}.` 
);
// Thanks for your purchase, Kyle! 
// Your product cost was $12.00, which with tax comes out to $14.40.

注意: 这里并没有写成如下形式:

var text = dollabillsyall
    `Thanks for your purchase, ${name}! 
    Your product cost was ${amt1}, 
    which with tax comes out to ${amt1 * 1.20}.` 
console.log(text);

因为 chrome 控制台不支持这样的打印测试,总是会抛出text已经被声明的错误。

模块

  • ES6 使用基于文件的模块,也就是说一个文件一个模块
  • ES6 模块的 API 是静态的,即需要在模块的公开 API 中静态定义所有最高层导入导出, 之后无法补充(如不能经过 if...else 判断后决定导入或导出谁)
  • ES6 模块是单例
  • 模块的公开 API 中暴露的属性和方法并不仅仅是普通的值或引用的赋值。它们是到内部模块定义中的标识符的实际绑定(几乎类似于指针)
  • 导入模块和静态请求加载这个模块相同,即导入需求可以静态扫描预先加载,甚至是在使用这个模块之前

导出

export 关键字的使用:

  1. 放在声明的前面
  2. 作为一个操作符与一个要导出的绑定列表一起使用

注意: export {...}import {...} 中的 { .. } 语法可能看起来像是一个对象字面量,甚至 import {...} from '' 看起来更像是一个对象解构语法。但这种形式是专用于模块的,所以注意不要把它和其他 { .. } 模式混淆。

关于导出,需要注意的一个困惑的地方:

// 错误写法
function foo() { .. }
export foo;
// export导出一个标识符,该标识符的名字正好是foo,
// 而这个foo与模块内部变量foo之间,没有一一对应的关系,就只是单纯的名字相同

// 正确写法1
export function foo() { .. };   // export 后面是声明语句,正确
// 正确写法2
function foo() { .. }
export { foo };     // 导出绑定列表,正确
// 正确写法3
function foo() { .. }
export default foo;  // 默认导出,导出的对外接口为default,正确

如果在你的模块内部修改已经导出绑定的变量的值,即使是已经导入的,导入的绑定也将会决议到当前(更新后)的值。

var awesome = 42;
export { awesome };
awesome = 100;

导入这个模块的时候,导入的绑定就会决议到 100 而不是 42。因为本质上,绑定是一个指向 awesome 变量本身的引用或者指针,而不是这个值的复制。

注意: 关于默认导出有一个微妙的细节需要格外小心。比较这两段代码:

function foo(..) {
 // ..
}
export default foo;
// ========分割线========
function foo(..) {
 // ..
}
export { foo as default };

解析差异:

  1. export default .. 接受一个表达式(目前还不能放声明语句),导出的是此时表达式的值,如果之后在你的模块中给 foo 赋一个不同的值,模块导入得到的仍然是原来导出的函数,而不是新的值。
  2. export { foo as default },导出的是对于foo的绑定,如果之后修改了 foo 的值,在导入一侧看到的值也会更新

所以要小心这个微妙的陷阱,根据自己的需要去决定使用哪一种。

模块依赖环

所谓的模块依赖环,就是模块之间相互依赖。如 A 导入 BB 导入 AAB 是相互依赖的关系。首先一点,要尽量避免故意设计带有环形依赖的系统。其次,解释下不得不这样设计的情况下,内部的工作原理:

假设这里有模块 AB 相互依赖:

  1. 先加载模块 A,第一步是扫描这个文件分析所有的导出,这样就可以注册所有可以导入的绑定。然后处理 import .. from "B",这表示它需要取得 B,此处等待。
  2. 引擎然后加载模块 B ,会对它的导出绑定进行同样的分析。当看到 import .. from "A", 它已经了解 AAPI,所以可以验证 import 是否有效。现在它了解了 BAPI,就可以验证等待的 A 模块中 import .. from "B" 的有效性。

import 语句的静态加载语义意味着可以确保通过 import 相互依赖的模块在其中任何一个运行之前,二者都会被加载、解析和编译。

extends 提供了一个语法糖,用来在两个函数原型之间建立 [[Prototype]] 委托链接。如 Bar extends Foo 的意思是把 Bar.prototype[[Prototype]] 连接到 Foo.prototype。不要错误的理解为 "原型继承"

super 恶龙

super 的行为根据其所处的位置不同而有所不同,而其中的 this也不适用 4 条绑定规则,此时的 this 值更像是静态设定好的。下面列出要点:

  1. super 作为函数调用时,代表父类的构造函数,但此时 super 内部的 this 指向当前子类实例
  2. super 作为对象时,在普通方法中,指向父类的原型对象

ES6 规定,在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例

  1. super 作为对象时,在静态方法中,指向父类

在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前子类

子类构造器

对于类和子类来说,构造器并不是必须的;如果省略的话那么二者都会自动提供一个默认构造器。但是,这个默认替代构造器对于直接类和扩展类来说有所不同。

直接类的默认构造器:

class Point {
}
// 等同于
class Point {
    constructor() {}
}

扩展类的默认构造器:

class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
    constructor(...args) {
        super(...args);
    }
}

构造器 constructor 方法默认返回实例对象(即this),但也完全可以指定返回另外一个对象(考虑下new的实现过程,是不是有似曾相识的感觉)。

扩展原生类

新的 classextends 设计带来的最大好处之一是(终于 !)可以构建内置类的子类了。

class MyCoolArray extends Array {
    first() { return this[0]; }
}
var a = new MyCoolArray( 1, 2, 3 );
a.length; // 3
a; // [1,2,3]
a.first(); // 1

ES6 之前,有一个 Array 的伪“子类”通过手动创建对象并链接到 Array.prototype,只能部分工作。它不支持真正 Array 的特有性质,比如自动更新 length 属性。ES6 子类则可以完全按照期望“继承”并新增特性!

集合

MapSet

  • Mapset()方法和 Setadd()方法,需要注意:设置值时,两个方法除都认为 +0-0 相等外,其中的比较算法和 Object.is(..) 几乎一样;二者都有的 has() 方法,也是这样。
  • Map 设置键值与 Set 设置值时,都不允许强制类型转换

WeakMap

WeakMapMap 的变体,二者的多数外部行为特性都是一样的,区别在于内部内存分配 (特别是其 GC)的工作方式。
WeakMapMap的两点区别:

  1. WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名
  2. WeakMap的键名所指向的对象是被弱持有的,不计入垃圾回收机制

注意: WeakMap 只是弱持有它的键,而不是值。
加深印象,如下:

var m = new WeakMap();
var x = { id: 1 },
    y = { id: 2 },
    z = { id: 3 },
    w = { id: 4 };
m.set( x, y );
x = null; // { id: 1 } 可GC
y = null; // { id: 2 } 可GC
          // 只因 { id: 1 } 可GC
m.set( z, w );
w = null; // { id: 4 } 不可GC,因为 { id: 3 } 不可GC

WeakSet

WeakSetWeakMap 类似,只是没有键而已,它的值也必须是对象。

var s = new WeakSet();
var x = { id: 1 },
    y = { id: 2 };
s.add( x );
s.add( y );
x = null; // x可GC
y = null; // y可GC

新增 API

Array

  • Array.of(..)Array.from(..),二者都以与构造器类似的方式创建一个新数组。在子类型方面它们会创建继承子类型的实例。如下:
class MyCoolArray extends Array {
    ...
}
MyCoolArray.from( [1, 2] ) instanceof MyCoolArray; // true
Array.from(
    MyCoolArray.from( [1, 2] )
) instanceof MyCoolArray; // false
Array.from(
    MyCoolArray.from( [1, 2] )
) instanceof Array; // true

所有的内置类(比如 Array)都有定义,任何创建新实例的原型方法都会使用 @@species 设置。可以使用如下:

class MyCoolArray extends Array {
 // 强制species为父构造器
    static get [Symbol.species]() { return Array; }
}
var x = new MyCoolArray( 1, 2, 3 );
x.slice( 1 ) instanceof MyCoolArray; // false
x.slice( 1 ) instanceof Array; // true

注意: @@species 设置只用于像 slice(..) 这样的原型方法,of(..)from(..)不会使用它,因为这两个方法是直接挂载在 Array 对象上的;它们都只使用 this 绑定(由使用的构造器来构造其引用)。

  • find() 方法会返回实际的数组值或undefined,区别于 some()、every()方法返回布尔值
  • findIndex() 方法解决了 indexOf() 方法无法控制匹配逻辑,总是使用 === 严格相等的问题,两个方法的返回值相同
  • find()findIndex()方法的第一参数都是回调函数,第二可选参数作用都为改变回调函数this的指向

Object

  • Object.assign() 众所周知,合并对象到指定对象,浅复制,注意点是复制时忽略不可枚举的属性,且只读属性被复制时是作为一个普通属性被复制(如复制前数据属性是writable: false但复制后是writable: true)。

Number

首先给出方法对比:

parseInt === Number.parseInt;   // true
parseFloat === Number.parseFloat;   // true
isFinite === Number.isFinite;   // false
isNaN === Number.isNaN;     // false
  • Number.isNaN() 修复了 isNaN() 对非数字的东西都返回 true,而不只对真实的 NaN 值返回 truebug。原因:isNaN() 会把参数强制转换为数字类型,而 Number.isNaN() 则不会。
  • Number.isFinite() 同样修复了 isFinite() 会对参数进行强制类型转换最终所导致的 bug

元编程

元编程是指操作目标是程序本身的行为特性的编程。

  • 元编程关注以下一点或几点:代码查看自身、代码修改自身、代码修改默认语言特性,以 此影响其他代码。
  • 元编程的目标是利用语言自身的内省能力使代码的其余部分更具描述性、表达性和灵活 性。

函数名称

你的代码在有些情况下可能想要了解自身,想要知道某个函数的名称是什么。name 属性可以获取函数名称,它是用于元编程目的的。对于不同情况下 name 的值是不尽相同的,这里直接引用(虽然很多):

(function(){ .. });     // name:"" (空字符串)
(function*(){ .. });    // name:"" (空字符串)
window.foo = function(){ .. };  // name:"" (空字符串)
class Awesome {
    constructor() { .. } // name: Awesome
    funny() { .. }       // name: funny
}
var c = class Awesome { .. }; // name: Awesome
var o = {
    foo() { .. },   // name: foo
    *bar() { .. },  // name: bar
    baz: () => { .. }, // name: baz
    bam: function(){ .. }, // name: bam
    get qux() { .. }, // name: get qux
    set fuz() { .. }, // name: set fuz
    ["b" + "iz"]: function(){ .. }, // name: biz
    [Symbol( "buz" )]: function(){ .. } // name: [buz]
};
var x = o.foo.bind( o ); // name: bound foo
(function(){ .. }).bind( o ); // name: bound 
export default function() { .. } // name: default
var y = new Function(); // name: anonymous
var GeneratorFunction =
 function*(){}.__proto__.constructor;
var z = new GeneratorFunction(); // name: anonymous

元属性

元属性以属性访问的形式提供特殊的其他方法无法获取的元信息

new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令或 Reflect.construct() 调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的。
Tips: Reflect.construct方法等同于 new target(...args),这提供了一种不使用 new,来调用构造函数的方法。

公开符号

公开符号即 JavaScript 预先定义了一些内置符号。比较常用的如下:

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.species
  • Symbol.iterator
  • Symbol.toPrimitive
  • Symbol.toStringTag

这里不展开介绍,直接放上阮老师的链接:内置的 Symbol 值

代理(Proxy

代理是一种由你创建的特殊的对象,它“封装”另一个普通对象——或者说挡在这个普通对象的前面。你可以在代理对象上注册特殊的处理函数,代理上执行各种操作的时候会调用这个程序。这些处理函数除了把操作转发给原始目标 / 被封装对象之外,还有机会执行额外的逻辑。

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);

target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。

Proxy 支持的拦截操作见 Proxy 实例的方法

代理局限性

如下的一些底层基本运算目前还不支持拦截:

typeof obj;
String( obj );
obj + "";
obj == pobj;
obj === pobj

可取消代理

普通代理总是陷入到目标对象,并且在创建之后不能修改。若想要创建一个在想要停止它作为代理时便可以被停用的代理,可以使用可取消代理。如下创建:

let {proxy, revoke} = Proxy.revocable(target, handler);
revoke();   // 取消代理

返回值为一个有两个属性——proxyrevode 的对象。proxy 属性是 Proxy 实例,revoke 属性是一个函数,可以取消 Proxy 实例。当代理取消之后,再访问Proxy实例,就会抛出一个错误。

使用代理

两种用法: 代理在先 or 代理在后。

  • 代理在先即通常的做法把代理看作是对目标对象的“包装”。此时代理成为了代码交互的主要对象,而实际目标对象保持隐藏 / 被保护的状态。
  • 代理在后即把 proxy 对象放到主对象的 [[Prototype]] 链中,让代理只作为最后的保障。

我们可以使用代理来为访问对象不存在属性时增加防御性(即访问对象不存在属性时使其抛出错误),还可以来模拟或扩展 [[Prototype]] 机制的概念。总之代理使得很多其他威力强大的元编程任务成为可能。

Reflect API

Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

Reflect 方法应该总是出现在代理中,来确保完成原有的行为,可以在完成原有行为前后部署额外的功能,完成代理的目的。所以一般 ReflectProxy 总是成对出现。

ES2020

只列出我知道的且已经是标准的: ECMAScript proposal at stage 4 of the process。这里只简单介绍。

链判断运算符

链判断运算符 ?. 简化了在读取对象内部的某个属性时为了确定对象属性是否存在的写法。
实 🌰 见分晓:

// 以前
const A = (foo && foo.info && foo.info.name) || 'default';
// 现在
const A = foo?.info?.name || 'default'; 

Null 判断运算符

Null 判断运算符 ?? 解决了 逻辑或运算符 || 存在的问题,即属性的值如果为空字符串或false、0、NaN,默认值也会生效。原因:|| 操作符, 若第一个值为假,返回第二个值。

也就是说只有 ?? 运算符左侧的值为 nullundefined 时,才会返回右侧的值。
实 🌰 见分晓:

// 以前
'' || 'default';    // 'default'
false || 'default'; // 'default'
0 || 'default'; // 'default'
// 现在
'' ?? 'default';    // ""
false ?? 'default'; // false
0 ?? 'default'; // 0
// null 或 undefined 情况
null ?? 'default';    // 'default'
undefined ?? 'default';    // 'default'

注意: 再强调一遍,唯一的假值对象 document.all

document.all || 'default';  // 'default'

Promise.allSettled()

该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是 fulfilled 还是 rejected,包装实例才会结束。

该方法返回的新的 Promise 实例,一旦结束,状态总是 fulfilled,不会变成 rejected。它的监听函数接收到的参数是数组,该数组的每个成员都是一个对象,对应传入的每个 Promise 实例。每个对象都有status属性,该属性的值只可能是字符串fulfilled或字符串rejectedfulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。

BigInt

BigInt 是一种新的数据类型。它只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。为了与 Number 类型区别,BigInt 类型的数据必须添加后缀 n

typeof 123n;  // "bigint"
12n === 12 // false

const a = 2172141653n;
const b = 15346349309n;
// BigInt 可以保持精度
a * b // 33334444555566667777n

JavaScript 原生提供 BigInt 对象,可以用作构造函数生成 BigInt 类型的数值。

BigInt(123) // 123n
BigInt('123') // 123n
BigInt(false) // 0n
BigInt(true) // 1n

注意: BigInt() 构造函数必须有参数,而且参数必须可以正常转为数值,且这个数值不能是小数。

动态引入

ES2020 引入了 import() 函数,支持动态加载模块(如可以按条件加载(if...else))。import() 类似于 Noderequire 方法,区别主要是前者是异步加载,后者是同步加载。

结语

四十多天以来,我利用晚上和周末时间,读完了《你不知道的JavaScript》系列丛书的上中下三卷,我只能说这系列的书太具有吸引力了,它打开了我 Javascript世界 的大门。读书的时候,我怕自己读得快了,匆匆理解就跳过,所以就要求自己写总结做笔记,以此来加强记忆和理解。当然不可避免的是,学的越多遗忘的也越多,所以有时候我不得不再去捡回从脑袋中丢出的知识。写到这里,我突然也希望有一天能像 Kyle Simpson (本书的作者)一样,说出:"我忘记的JavaScript比大多数人学过的还多"这样的话来,当然这更多的只是个梦想罢了,我不得不承认的是我并不是一个善于学习的人,还有万一哪天我厌倦了办公室不想做程序员而只想去送外卖呢?

阮一峰的ECMAScript 6 入门网站,我不知道浏览了多少遍,直到现在还是,但我明显感觉相比四十天之前,其中的不理解之处减少了,但依旧有(比如正则、ArrayBuffer等讨厌的东西,因为我认为自己工作接触较少所以选择了跳过),这些在未来的日子应该会逐步减少,能让你真正铭刻于心的是那些你经常反复去用的知识,不经常使用的就算学习了,意义也不大,时间一长,最终也还是会丢出脑袋,所以有些东西只是在需要应急时才去学习,而这个学习也应该只会是粗略的。学习是一个循序渐进的过程,前人呕心沥血写出来的知识结晶,是值得你不断反复去学习理解的(比如设计模式)。

这里总结一下JS中应该反复学习加深理解的要点:

  • 词法作用域
  • 闭包(closure
  • this
  • 对象原型
  • 类型转换和语法
  • 异步:Promise + Generator

论学习JS,我认为没有比《JavaScript高级程序设计》+ 《你不知道的JavaScript》更好的选择了:一本入门 + 三本深入理解。对于JS核心基础(不包括dom)的学习,我就先搞到这里吧。相信随着工作中的应用实践,对于JS的理解会越来越深,一些难点会越来越清晰,遗忘也会减少。以后如果有时间,我希望自己能再读一遍,但目前按照个人学习路线,后面我要去开启数据结构与算法之旅了,或许现在我只是希望较快得先将关于前端的知识都系统的学习一遍。

我们大多数人,不得不承认的一点是:我们天生不聪明。但科学证明了一点:认为努力就能变聪明的人比自认为自己天生就聪明的人,在各方面表现的更好。因为自认为自己天生聪明的人,就认定自己比别人聪明,从而在各方面表现不如别人努力。聪明也是可以后天学习的,不要让爱迪生名言后面那半句话(那1%的灵感是最重要的,甚至比那99%的汗水都要重要)成为你不努力的理由,天才不努力也是白费,要坚信普通人也能做不普通的事。