Hotaru's Notebook

我不知道的JS

《你不知道的JS》 (You don’t know JS) 读书笔记

Preface

一直以为 JavaScript 就是 Script 版的 Java,但在看了这本书后才知道,它丫的就是个 Script,跟 Java 一点儿关系都没有。 JS 有一些 Java 没有的特性,比如:

我本来还想指望看完这系列书后能对 JS 的刻板印象有所改观,然而事与愿违。 下面我将以一个水平不及初级Lava程序员的程序员的角度,为本书做一份笔记。

作用域和闭包(Scope & Closures)

1. 变量作用域因 var 而变得奇葩

先看代码:

function fn() {
    {
        var num = 1;
    }
    console.log(num); // Output: 1
}

function fn2() {
    console.log(num); // Uncaught ReferenceError: num is not defined
}

在 Java 里,num 离开大括号后就应该是未定义的了,然而 JS 却唱反调。解决办法也很简单,使用 ECMAScript 6 的新关键字 let 即可,用法和 var 完全一致 但不会有 var 那种奇葩的作用域。

function fn() {
    {
        let num = 1;
    }
    console.log(num); // Uncaught ReferenceError: num is not defined
}

function fn2() {
    console.log(num); // Uncaught ReferenceError: num is not defined
}

另外 fn2() 无论如何都是抛出 ReferenceError,说明 var 的作用域仅限于 function 内。 总结:

  1. 能用 let 就不要用 var
  2. let 的作用域在它所在的大括号内,var 的作用域在它所在的 function

1.1 用淫技"伪造"一个块作用域(Block Scope)

正如书中所述,JS的块作用域很模糊。既然函数可以产生一个作用域,那么 借助它就可以不使用 let 关键字来产生一个 “块作用域”:

(function(param1) {
    var unaccessible = true;
    console.log(param1);
})("Hello world"); // 会输出 "Hello world"
console.log(unaccessible); // ReferenceError

上面的代码的格式大概是这样的:

(/*该括号里声明函数*/)(/*该括号使得左边括号里的代码立即执行*/);

2. 提升(Hoisting)

TL;DR: 使用任何变量前请先把它定义出来,这点对于 Java 程序员来讲很容易吧。

在看完书中 Hoisting 部分后,我的理解是这样的:任意位置定义出来的东西(包括但不限于变量、function)都会把 定义(Definition) 挪到函数最开始的位置。就像下面这样:

function fnTest() {
    console.log("Hello, ");
    let someone = "Walter White";
    console.log(someone);
}

经过 Hoisting 后就会变成这样:

function fnTest() {
    let someone;
    
    console.log("Hello, ");
    someone = "Walter White";
    console.log(someone);
}

其实"提升"是一种误解。我觉得把 StackOverflow 上的 这个问题的答案 翻译一下来得更直接:

As Stoyan Stefanov explains in “JavaScript Patterns” book, the hoisting is result of JavaScript interpreter implementation. Stoyan Stefanov 在 《JavaScript Patterns》 一书中解释道,这种 Hoisting 现象其实是由 JS 的解释器的实现导致的。

The JS code interpretation performed in two passes. During the first pass, the interpreter processes variable and function declarations. 解释器解释 JS 代码的时候会把 JS 代码过两遍。第一遍处理所有的变量和函数定义。

The second pass is actually code execution step. The interpreter processes function expressions and undeclared variables. 第二遍在才会执行具体的 JS 代码,处理函数表达式以及未定义的变量。

Thus, we can use the “hoisting” concept to describe such behavior. 因此,我们可以用 “提升” 这个概念来形容这种行为。

Hope this helps. - lxgreen answered Feb 22 ‘13 at 0:22

I personally really do not like the word “hoisting”. It gives the false representation that variable and function declarations are magically hoisted to the top of the current scope, when in reality, the JS interpreter, as you mentioned, scans the source code for the bindings and then executes the code. – contactmatt Feb 24 ‘13 at 22:32 我个人非常不喜欢 “提升” 这个词。它会给人一种错误的观念,让人以为变量和函数的定义都被神奇的提升到了当前作用域最开始的位置。但实际情况是,正如你所提到的,JS 解释器会先扫描代码中的所有绑定(这里绑定指的是把函数或变量绑定到某个变量名上)然后再执行具体的代码。

3. 闭包

强烈建议阅读原书,不过我在这里依然会做简单的介绍。

function fnTest(param0) {
    $(".button").on("click", function() {
        console.log(param0);
    });
}

用鼠标点击 .buttonconsole.log() 可以访问 param0,这就是闭包。

this 和对象原型(Object Prototype)

1. 半残废的 OOP

以我目前的理解:

  1. JS 的 protytope 就是 Java 的类
  2. JS 的 prototype 链就是 Java 的类继承

不过书中反复强调 JS 里没有类的概念。

// 定义一个类的构造函数。是的,先定义构造函数,
var Clazz = function Clazz() {
    console.debug("Clazz()");
};
// 然后再继续编写类的行为。很可惜的是,像下面这样定义类的行为的话,多态也就无从谈起了。
Clazz.prototype.greeting = function greeting(someone) {
    console.log("Hello, " + someone);
};
// 然后 ECMAScript 的研发人员们又搞了这个:
Clazz.prototype.getArguments = function getArguments() {
    return arguments;// 请把 arguments 对象当作数组用,然后通过数组下标获取调用当前函数时所传入的参数,下标指向不存在的东西会返回 undefined.
};
// 实例化一个 Clazz "类"
let clazz = new Clazz();
clazz.greeting("Walter White");// 输出 "Hello, Walter White"
for(let p of clazz.getArguments("Hello", "Walter White")) {
    console.log(p);
    /* 输出:
    Hello
    Walter White
    */
}

改用 ES6 语法来定义类:

// 定义类 Clazz
class Cooker {
    // 构造函数不能重载
    constructor() {
        console.log(this);
        this.someone = arguments[0];
        console.log("Cooker.constructor", arguments);
    }
    // 普通的函数也不能重载
    greeting() {
        console.log("Hello, " + this.someone);
    }
}
class WalterWhite extends Cooker {
    constructor() {
        super("Walter White");
        console.log("WalterWhite.constructor");
    }
}
// 使用方法
let cooker = new Cooker("Walter White");
let walterWhite = new WalterWhite();
cooker.greeting();// 输出 "Hello, Walter White"
cooker.greeting("Walter White"); // 输出得和上一行代码一样
walterWhite.greeting();

其实到头来还是因为 JS 的半残废的面向对象系统,要多态没有多态,要静态变量没有静态变量,要访问控制没有访问控制。要我说吧,Script 就做好你 Script 的本职工作就好了,没有 Python 的实力还非要学别人搞 OOP。

2. 抱歉,没有静态类和静态变量

是的,没有静态类也没有静态变量,就如同书中 “this和对象原型 -> 附录 A.2” 里说的一样。 想写静态变量的话,拿全局变量代替吧,在类里写静态变量事倍功半。

3. this is not this.

JS 里的 this 和 Java 里的 this 可谓是天壤之别,强烈建议阅读原书(原书里句句命中要害,几乎没有废话),下面借用第2章的总结:

  1. 由 new 调用绑定到新创建的对象。
  2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。

未完待续…

更新历史

10 Apr 2017:首次发布 13 Apr 2017:

References

  1. Why does JavaScript hoist variables? - stackoverflow.com

    As Stoyan Stefanov explains in “JavaScript Patterns” book, the hoisting is result of JavaScript interpreter implementation. The JS code interpretation performed in two passes. During the first pass, the interpreter processes variable and function declarations. The second pass is actually code execution step. The interpreter processes function expressions and undeclared variables. Thus, we can use the “hoisting” concept to describe such behavior. Hope this helps. - lxgreen answered Feb 22 ‘13 at 0:22

    I personally really do not like the word “hoisting”. It gives the false representation that variable and function declarations are magically hoisted to the top of the current scope, when in reality, the JS interpreter, as you mentioned, scans the source code for the bindings and then executes the code. – contactmatt Feb 24 ‘13 at 22:32

#JavaScript #You don't know JS