chapter2-into-javascript

You Don't Know JS: Up & Going

Chapter 2: Into JavaScript

在前一章中,我介绍了编程的基本构建块,例如变量,循环,条件和函数。当然,所有显示的代码都是用JavaScript编写的。但在本章中,我们希望专注于有关JavaScript的知识,以便成为JS开发人员。

在本章中,我们将介绍一些概念,后面的YDKJS书籍中才会充分探讨这些概念。你可以将本章视为本系列其余部分详细介绍的主题概述。

特别是如果你是JavaScript的新手,那你应该花更多的时间来多次查看这些概念和代码示例。任何好的基础都是一砖一瓦的,所以不要指望你会在第一次就立即理解它。

从这里开始深入学习JavaScript的过程。

注意: 正如我在第1章中所说的,当你阅读并完成本章时,你一定要自己尝试所有这些代码。请注意,这里的一些代码假定在编写本文时最新版本的javascript中引入了一些功能(对于ECMAScript的第6版(JS规范的正式名称)通常称为“ES6”)。如果碰巧使用较旧的ES6之前的浏览器,则代码可能无法正常工作。应该使用最新的现代浏览器(如Chrome,Firefox或IE)。

Values & Types

正如我们在第1章中断言的那样,JavaScript有类型值,而不是类型变量。可以使用以下内置类型:

  • string

  • number

  • boolean

  • null and undefined

  • object

  • symbol (ES6新加的)

JavaScript提供了typeof操作符,可以检查一个值并告诉你它是什么类型的:

var a;
typeof a;                // "undefined"

a = "hello world";
typeof a;                // "string"

a = 42;
typeof a;                // "number"

a = true;
typeof a;                // "boolean"

a = null;
typeof a;                // "object" -- weird, bug

a = undefined;
typeof a;                // "undefined"

a = { b: "c" };
typeof a;                // "object"

typeof运算符的返回值始终是六个中的一个(ES6中的七个! - "symbol"类型)字符串值。也就是说,typeof "abc"返回"string",而不是string

请注意,在这段代码中,a变量是如何保存每种不同类型的值的,并且,尽管出现了这种情况,typeof a并不是询问求“a的类型”,而是询问“a中当前值的类型”。只有值在JavaScript中有类型;变量只是这些值的简单容器。

typeof null是一个有趣的案例,因为他错误的返回"object",你可能期待他返回的是"null"

警告: 这是JS中一个长期存在的错误,但可能永远不会被修复。Web上的代码过多依赖于这个bug,因此修复它会导致更多错误!

另外,请注意a = undefined。我们明确地将a设置为undefined的值,但这与没有设置值的变量在行为上没有区别,就像在代码段顶部的var a;行。变量可以通过几种不同的方式获得"未定义"值状态,包括不返回值的函数和void运算符的用法。

Objects

object类型是指一个复合值,你可以在其中设置属性(命名位置),每个属性都包含自己的任何类型的值。这可能是所有JavaScript中最有用的值类型之一。

var obj = {
    a: "hello world",
    b: 42,
    c: true
};

obj.a;        // "hello world"
obj.b;        // 42
obj.c;        // true

obj["a"];    // "hello world"
obj["b"];    // 42
obj["c"];    // true

可视化的考虑这个obj可能会有帮助:

可以用点表示法(即obj.a)或括号表示法(即obj ["a"])访问属性。点符号表示较短并且通常更易于阅读,因此在可能的情况下是优先选择的。

如果你的属性名称中包含特殊字符,则括号表示法非常有用,例如obj["hello world!"] - 当通过括号表示法访问时,这些属性通常称为 []表示法要求变量(下面解释)或string字面量(就是需要用".."'..'包装)。

当然,如果要访问属性/键但这个名称存储在另一个变量中,括号表示法也很有用,例如:

var obj = {
    a: "hello world",
    b: 42
};

var b = "a";

obj[b];            // "hello world"
obj["b"];        // 42

注意: 有关JavaScript对象的更多信息,请参阅本系列的 this&Object Prototypes ,特别是第3章。

在JavaScript程序中,还有一些其他的值类型通常会与之交互:数组函数 。但是,这些类型不应该是内置类型,而应该被认为更像子类型——object类型的专门版本。

Arrays

数组是一个object,它保存的值(任何类型)不是在属性/键的中,而是在数字索引位置中。例如:

var arr = [
    "hello world",
    42,
    true
];

arr[0];            // "hello world"
arr[1];            // 42
arr[2];            // true
arr.length;        // 3

typeof arr;        // "object"

注意: 从零开始计数的语言使用0作为数组中第一个元素的索引, JS就是这样做的。

从视觉上考虑arr可能会有所帮助:

因为数组是特殊对象(如typeof暗示的),所以它们也可以具有属性,包括自动更新的length属性。

理论上,你可以将数组用作具有自己命名属性的普通对象,或者你可以使用object但只给它类似于数组的数字属性(0,1等)。然而,这通常被认为是对各种类型的不当使用。

最好也是最自然的方法是将数组用于数字定位值,并将object用于命名属性。

Functions

你将在JS程序中使用的另一个object子类型是一个函数:

function foo() {
    return 42;
}

foo.bar = "hello world";

typeof foo;            // "function"
typeof foo();        // "number"
typeof foo.bar;        // "string"

同样,函数是object的子类型 - typeof返回"function",这意味着function是主要类型 - 因此可以具有属性,但通常只在有限的情况下使用函数对象属性 (如 foo. bar)。

注意: 有关JS值及其类型的更多信息,请参阅本系列的 Types&Grammar 的前两章。

Built-In Type Methods

我们刚刚讨论过的内置类型和子类型将行为暴露为非常强大和有用的属性和方法。

例如:

var a = "hello world";
var b = 3.14159;

a.length;                // 11
a.toUpperCase();        // "HELLO WORLD"
b.toFixed(4);            // "3.1416"

背后是如何能够调用a.toUpperCase()比仅存在于值上的方法更复杂。

简而言之,有一个String(大写字母S)对象包装器形式,通常称为“native(原生)”,与原始string类型配对;这个对象包装器在其原型上定义了toUpperCase()方法。

当你通过引用属性或方法(例如,前一个代码段中的a.toUpperCase())将像"hello world"这样的原始值作为object时,JS会自动将值“装箱”到其对象包装器对应物(隐藏在引擎盖里)。

string值可以被String对象包装,number可以被Number对象包装,boolean可以被Boolean对象包装。在大多数情况下,你不需要担心或直接使用这些值的对象包装形式 - 在几乎所有情况下都更喜欢原始值形式,JavaScript将为你处理剩下的事情。

注意: 有关JS 原生和"装箱"的更多信息,请参阅本系列的 Types&Grammar 的第3章。要更好地理解对象的原型,请参阅本系列的 this&Object Prototypes 的第5章。

Comparing Values

在JS程序中,需要进行两种主要的值比较:相等不相等 。无论比较的值是什么类型,任何比较的结果都是布尔值(truefalse)。

Coercion

我们在第1章中简要介绍了强制行为,但让我们在这里重温一下。

强制在JavaScript中有两种形式:显式隐式 。显式强制只是说你可以从代码中明显地看到从一种类型到另一种类型的转换将发生,而隐式强制是指类型转换可能更多地发生在某些其他操作的非显而易见的副作用上。

你可能已经听说过“强制是邪恶的”这样带情绪的说法,因为这很明显,在某些地方,强制可以产生一些令人惊讶的结果。也许没有什么比语言让开发人员感到吃惊更能引起他们的沮丧了。

强制不是邪恶的,也不是令人惊讶的。实际上,你可以使用类型强制构造的大多数情况都非常明智且易于理解,甚至可以用于提高代码的可读性。但是我们不会进一步讨论这个问题 - 本系列第4章的 Types & Grammar 涵盖了所有方面。

这是一个 显示 强制的例子:

var a = "42";

var b = Number( a );

a;                // "42"
b;                // 42 -- the number!

这是 隐式 强制的一个例子:

var a = "42";

var b = a * 1;    // "42" implicitly coerced to 42 here

a;                // "42"
b;                // 42 -- the number!

Truthy & Falsy

在第1章中,我们简要地提到了值的"truthy"和"falsy"性质:当一个非布尔值被强制转换为布尔值时,它是分别变为true还是false

JavaScript中"falsy"值的具体列表如下:

  • "" (空字符串)

  • 0, -0, NaN (无效 number)

  • null, undefined

  • false

任何不在这个"falsy"名单上的值都是"truthy"。以下是一些例子:

  • "hello"

  • 42

  • true

  • [ ], [ 1, "2", 3 ] (数组)

  • { }, { a: 42 } (对象)

  • function foo() { .. } (函数)

重要的是要记住,如果它实际上被强制转换为boolean,则非boolean遵循这种"truthy"/"falsy"强制。困惑住你自己并不困难 —— 当看起来像是将一个值强制转换为boolean,可实际上它不是。

Equality

这里有四个相等运算符:==, ===, !=, 和 !==。这个!形式当然是对应的"不相等"版本,不相等(non-equality) 不应该与 不等式(inequality) 混淆。

=====之间的区别通常变现为==检查值相等,===检查值和类型相等。但是,这说法是不准确的。表述它们的正确方法是==在允许强制的情况下检查值相等,并且===检查值相等不允许强制;因此,===通常被称为"严格相等"。

考虑下允许隐式强制的==宽松相等比较 和 不允许隐式强制比较的===严格相等:

var a = "42";
var b = 42;

a == b;            // true
a === b;        // false

a == b比较中,JS注意到类型不匹配,因此它通过一系列有序的步骤将一个或两个值强制转换为不同的类型,直到类型匹配,然后可以检查简单的值相等性。

如果你考虑一下,a == b有两种可能的方式通过强制得到true。比较可能最终为42 == 42或者可能是"42"=="42"。那是哪个呢?

答案是:"42"变成42,造成比较的是42 == 42。在这样一个简单的例子中,这个过程走向哪个方面似乎并不重要,因为最终结果是相同的。有更复杂的情况,重要的不仅仅是比较的最终结果是什么,而是你 如何 实现目标

a === b产生的结果是false,因为强制转换不被允许,所以简单的值比较明显失败了。许多开发人员认为===更具可预测性,因此他们主张始终使用该形式并远离==。我认为这种观点非常短浅。我相信==是一个强大的工具并且可以帮助你的程序,前提是你花时间了解它的 工作原理

我们不打算在这里详细介绍==比较中的强制是如何工作的。其中很多都是非常合理明智的,但有一些重要的极端用例需要注意。你可以阅读ES5规范的第11.9.3节(http://www.ecma-international.org/ecma-262/5.1/)以查看确切的规则,而且与围绕它的所有负面炒作相比,你会惊讶于这种机制是多么直接。

为了将大量的细节归结为几个简单的要点,并帮助你了解在各种情况下是使用==还是===,下面是我的简单规则:

  • 如果比较中的任一值(也称为 侧)可能是truefalse值,则避免==并使用===

  • 如果比较中的任何一个值可能是这些特定值之一(0""[] -- 空数组),则避免==并使用===

  • 在所有其他情况下,可以安全地使用==。它不仅安全,而且在许多情况下,它以提高可读性的方式简化了代码。

这些规则 归结为要求你批判性地思考你的代码,以及哪些类型的值可以通过变量来进行相等比较。如果你可以确定值,那么==是安全的,请使用它!如果你无法确定值,请使用===。就这么简单。

!=非相等形式与==成对,而!=====成对。我们刚才讨论过的所有规则和注意点都是对称地进行这些非等式比较。

如果要比较两个非基元值,如object(包括functionarray),则应特别注意=====的比较规则。因为这些值实际上是通过引用来保存的,所以=====比较都只会检查引用是否匹配,而不是关于基础值的任何内容。

例如,默认情况下,通过简单地将所有值与逗号(,)连接,array被强制为string。可能认为具有相同内容的两个array==相等,但它们不是:

var a = [1,2,3];
var b = [1,2,3];
var c = "1,2,3";

a == c;        // true
b == c;        // true
a == b;        // false

注意: 有关==相等比较规则的更多信息,请参阅ES5规范(第11.9.3节),并参阅本系列的 Types & Grammar 的第4章;有关值与引用的更多信息,请参阅第2章。

Inequality

<,>,<=>=这些操作符用于不等式,参考规范中的名称为"关系比较"。通常,它们将与number这些通常可比的值一起使用。很容易理解3 <4

但是JavaScript的string值也可以被当做不等式来比较,使用典型的字母规则("bar"<"foo")。

那么关于强制转换呢?与==比较有相似的规则(尽管不完全相同!)适用于不等式运算符。值得注意的是,没有"严格的不等式"操作符可以像=== "严格的相等"那样禁止强制转换。

考虑下面的代码:

var a = 41;
var b = "42";
var c = "43";

a < b;        // true
b < c;        // true

发生了什么?在ES5规范的第11.8.5节中,它表示如果<比较中的两个值都是string,就像b < c一样,则按字典顺序进行比较(也就像字典一样按字母顺序排列)。但是如果其中一个或两个不是string,就像使用a < b一样,则两个值都被强制为number,并且会发生典型的数字比较。

你可能会遇到最大的问题是可能会在不同的值类型之间进行比较 - 请记住,没有“严格的不等式”形式可供使用 - 当其中一个值无法生成有效数字时,例如:

var a = 42;
var b = "foo";

a < b;        // false
a > b;        // false
a == b;        // false

等下,这三个比较怎么可能是false?因为b值在<>比较中被强制为“无效数值”NaN,并且规范说NaN既不大于也不小于任何其他值。

==比较失败的原因与之不同。如果它被解释为42 == NaN"42" == "foo" - a == b可能会失败。正如我们之前解释的那样,这里就是前一种情况。

注意: 有关不等式比较规则的更多信息,请参阅ES5规范的第11.8.5节,并参阅本系列的 Types & Grammar 的第4章。

Variables

在JavaScript中,变量名(包括函数名)必须是有效的 标识符 。当你考虑非传统字符(如Unicode)时,标识符中有效字符的严格和完整规则有点复杂。如果只考虑典型的ASCII字母数字字符,则规则很简单。

标识符必须以a-zA-Z$_开头。然后它可以包含任何这些字符加上数字0-9

通常,相同的规则适用于属性名,和变量标识符一样。但是,某些单词不能用作变量,但可以用作属性名。这些单词称为“保留字”,包括JS关键字(forinif等)以及nulltruefalse

注意: 有关保留字的更多信息,请参阅本系列的 Types & Grammar 的附录A.

Function Scopes

你可以使用var关键字声明一个属于当前函数作用域的变量,或者使用全局作用域(如果位于任何函数之外的顶层)。

Hoisting

var在作用域内出现的任何地方,该声明都属于整个作用域,并且随处可访问。

隐喻地,当var声明在概念上“移动”到此作用域的顶部时,这种行为称为 提升 。从技术上讲,这个过程可以通过编译代码的方式更准确地解释,但我们现在可以跳过这些细节。

考虑下面代码:

var a = 2;

foo();                    // 正常工作,因为 `foo()` 声明被提升了

function foo() {
    a = 3;

    console.log( a );    // 3

    var a;                // 声明被提升到`foo()`的顶部
}

console.log( a );    // 2

警告: 依靠变量提升在其作用域中使用var声明出现之前的变量是不常见的,也不是一个好主意;这可能会造成困惑。使用提升函数声明更为常见和被接受,就像我们在正式声明之前出现的foo()调用一样。

Nested Scopes

当你声明变量时,它可以在该作用域内的任何位置使用,也可以在任何较低/内部作用域内使用。例如:

function foo() {
    var a = 1;

    function bar() {
        var b = 2;

        function baz() {
            var c = 3;

            console.log( a, b, c );    // 1 2 3
        }

        baz();
        console.log( a, b );        // 1 2
    }

    bar();
    console.log( a );                // 1
}

foo();

请注意,cbar()内部不可用,因为它仅在内部的baz()作用域内声明,并且出于同样的原因,b不能用于foo()

如果你尝试在不可用的作用域内访问变量的值,则会引发ReferenceError。如果你尝试设置一个尚未声明的变量,你将最终在顶级全局范围内创建变量(糟糕的!)或得到一个错误,具体取决于“严格模式”(请参阅"Strict Mode")。让我们来看看:

function foo() {
    a = 1;    // `a` not formally declared
}

foo();
a;            // 1 -- oops, auto global variable :(

这是一个非常糟糕的实践。不要这样做!始终正式声明你的变量。

除了在函数级别创建变量声明之外,ES6还允许使用let关键字将变量声明为属于各个块({..})。除了一些微妙的细节,作用域规则的行为与我们刚看到的函数作用域大致相同:

function foo() {
    var a = 1;

    if (a >= 1) {
        let b = 2;

        while (b < 5) {
            let c = b * 2;
            b++;

            console.log( a + c );
        }
    }
}

foo();
// 5 7 9

因为使用let而不是varb将仅属于if语句,因此不属于整个foo()函数的作用域。同样,c只属于while循环。块作用域对于以更细粒度的方式管理变量作用域非常有用,这可以使代码随着时间的推移更容易维护。

注意: 有关作用域的更多信息,请参阅本系列的Scope & Closures 。有关let 块级作用域的更多信息,请参阅本系列的ES6 & Beyond

Conditionals

除了我们在第1章中简要介绍的if语句之外,JavaScript还提供了一些我们应该看一看的其他条件机制。

有时你会发现自己编写了一系列if..else..if语句,如下所示:

if (a == 2) {
    // do something
}
else if (a == 10) {
    // do another thing
}
else if (a == 42) {
    // do yet another thing
}
else {
    // fallback to here
}

这个结构可以工作,但它有点冗长,因为你需要为每个案例指定一个a的测试。这是另一个选项,使用switch语句:

switch (a) {
    case 2:
        // do something
        break;
    case 10:
        // do another thing
        break;
    case 42:
        // do yet another thing
        break;
    default:
        // fallback to here
}

如果你只希望运行一个case中的语句,则break很重要。如果在case中省略break,并且该case匹配或运行,则无论case匹配如何,都将继续执行下一个case的语句。这种所谓的“掉落”有时是有用/期望的:

switch (a) {
    case 2:
    case 10:
        // some cool stuff
        break;
    case 42:
        // other stuff
        break;
    default:
        // fallback
}

在这里,如果a210,它将执行"some cool stuff"代码语句。

JavaScript中的另一种条件形式是“条件运算符”,通常称为“三元运算符”。它就像一个if..else语句的更简洁形式,例如:

var a = 42;

var b = (a > 41) ? "hello" : "world";

// similar to:

// if (a > 41) {
//    b = "hello";
// }
// else {
//    b = "world";
// }

如果表达式(此处a> 41)的计算结果为true,则产生第一个子句("hello"),否则产生第二个子句("world"),然后将结果分配给b

条件运算符不必在赋值中使用,但这绝对是最常用的用法。

注意: 有关测试条件和其他模式(如switch, ? :)的更多信息,请参阅本系列的Types & Grammar

Strict Mode

ES5为该语言添加了“严格模式”,这加强了某些行为的规则。通常,这些限制被视为是使代码保持更安全和更合适的指导方针。此外,遵循严格模式使你的代码通常可以通过引擎进行优化。严格模式对于代码来说是一个巨大的胜利,你应该将它用于所有程序。

你可以为单个函数或整个文件选择严格模式,具体取决于你放置严格模式编译的位置:

function foo() {
    "use strict";

    // this code is strict mode

    function bar() {
        // this code is strict mode
    }
}

// this code is not strict mode

比较一下:

"use strict";

function foo() {
    // this code is strict mode

    function bar() {
        // this code is strict mode
    }
}

// this code is strict mode

使用严格模式的一个关键区别(改进!)不允许省略var而进行隐式自动全局变量声明:

function foo() {
    "use strict";    // turn on strict mode
    a = 1;            // `var` missing, ReferenceError
}

foo();

如果在代码中启用严格模式,你将会得到错误,或者代码开始出现bug,这可能会诱使你避免使用严格模式。但这种本能放纵是一个坏主意。如果严格模式会导致程序出现问题,那么几乎可以肯定,这表明你应该修复程序中的内容。

严格模式不仅可以使代码保持更安全,也不仅可以使你的代码更加优化,而且还可以代表语言的未来发展方向。现在习惯严格模式要比不断推迟容易得多——以后只会更难转换!

注意: 有关严格模式的更多信息,请参阅本系列的 Types & Grammar 的第5章。

Functions As Values

到目前为止,我们已经将函数作为JavaScript中 作用域 的主要机制进行了讨论。你记得典型的函数声明语法如下:

function foo() {
    // ..
}

虽然从语法中看起来似乎并不明显,但foo基本上只是外部封闭作用域中的一个变量,它给出了对所声明的function的引用。也就是说,function本身就是一个值,就像42[1,2,3]一样。

这听起来像是一个陌生的概念,所以花点时间思考一下。你不仅可以将值(参数)传递给函数,而且 函数 本身可以是赋给变量的值,也可以传递给其他函数或从其他函数返回。

因此,函数值应该被视为表达式,就像任何其他值或表达式一样。

考虑下面代码:

var foo = function() {
    // ..
};

var x = function bar(){
    // ..
};

分配给foo变量的第一个函数表达式称为匿名,因为它没有name

第二个函数表达式命名为(bar),即使对它的引用也被赋值给x变量。尽管 匿名函数表达式 仍然非常常见,但命名函数表达式 通常更为可取。

注意: 有关更多信息,请参阅本系列的 Scope&Closures

Immediately Invoked Function Expressions (IIFEs)

在前面的代码片段中,没有任何函数表达式被执行 - 例如,如果我们包含了foo()x(),我们就可以了。

还有另一种执行函数表达式的方法,通常称为 立即调用的函数表达式(IIFE):

(function IIFE(){
    console.log( "Hello!" );
})();
// "Hello!"

围绕(function IIFE(){..})函数表达式的外部(..)只是为了防止它被视为普通函数声明所需。

表达式末尾的最后一个() -- 就是})();这行,实际上是执行前面引用的函数表达式的。

这可能看起来很奇怪,但它并不像第一眼看上去那样陌生。考虑一下fooIIFE之间的相似之处:

function foo() { .. }

// `foo` function reference expression,
// then `()` executes it
foo();

// `IIFE` function expression,
// then `()` executes it
(function IIFE(){ .. })();

如你所见,在执行()之前列出(function IIFE() {..})与在执行()之前定义foo基本相同;在这两种情况下,函数引用都紧跟在()后面执行。

因为IIFE只是一个函数,函数创建变量作用域,所以以这种方式使用IIFE通常用于声明不会影响IIFE外部周围代码的变量:

var a = 42;

(function IIFE(){
    var a = 10;
    console.log( a );    // 10
})();

console.log( a );        // 42

IIFE还可以具有返回值:

var x = (function IIFE(){
    return 42;
})();

x;    // 42

42值从执行的IIFE命名函数return,然后分配给x

Closure

闭包 是JavaScript中最重要的概念之一,通常也是最不容易理解的概念之一。我不会在这里详细介绍它,而是引用本系列的 Scope&Closures。但是我想谈谈它的一些事情,以便你了解一般概念。它将是你的JS技能组中最重要的技术之一。

你可以将闭包视为一种“记住”并继续访问函数作用域(其变量)的方法,即使该函数已经完成运行。

考虑下面的代码:

function makeAdder(x) {
    // parameter `x` is an inner variable

    // inner function `add()` uses `x`, so
    // it has a "closure" over it
    function add(y) {
        return y + x;
    };

    return add;
}

每次调用外部makeAdder()时返回内部的add()函数引用都能记住传入makeAdder()的任何x值。现在,让我们使用makeAdder()

// `plusOne` 得到了一个内部的 `add(..)` 引用
// `add()` 函数拥有对外部 `makeAdder(..)` 的参数 `x` 的闭包
var plusOne = makeAdder( 1 );

// `plusOne` 得到了一个内部的 `add(..)` 引用
// `add()` 函数拥有对外部 `makeAdder(..)` 的参数 `x` 的闭包
var plusTen = makeAdder( 10 );

plusOne( 3 );        // 4  <-- 1 + 3
plusOne( 41 );        // 42 <-- 1 + 41

plusTen( 13 );        // 23 <-- 10 + 13

有关此代码如何工作的更多信息:

  1. 当我们调用makeAdder(1),我们得到一个对其内部add(..)的引用,它将x记为1。我们将此函数称为plusOne()

  2. 当我们调用makeAdder(10),我们得到了另一个对其内部add(..)的引用,它将x记为10。我们将此函数称为plusTen()

  3. 当我们调用plusOne(3),它将3(其内部y)添加到1(由x记住),结果得到4

  4. 当我们调用plusTen(13),它将13(其内部y)添加到10(由x记住),结果得到23

不要担心,如果第一次看起来很奇怪而且最让人感到困惑 - 这很有可能!完全理解它需要大量的练习。

相信我,一旦你这样做,它就是所有编程中最强大和最有用的技术之一。让你的大脑在闭包状态下思考一会儿,这绝对是值得的。在下一节中,我们将对闭包进行更多练习。

Modules

JavaScript中闭包最常见的用法是模块模式。模块允许你定义对外界隐藏的私有实现细节(变量,函数),以及可从外部访问的公共API。

考虑下面代码:

function User(){
    var username, password;

    function doLogin(user,pw) {
        username = user;
        password = pw;

        // do the rest of the login work
    }

    var publicAPI = {
        login: doLogin
    };

    return publicAPI;
}

// create a `User` module instance
var fred = User();

fred.login( "fred", "12Battery34!" );

User()函数用作外部作用域,用于保存变量usernamepassword,以及内部doLogin()函数;这些都是此User模块的私有内部细节,无法从外部访问。

警告: 我们并不是要在这里调用new User(),这是有意为之的(笔:字母大写),尽管对于大多数读者来说,实例化似乎更常见。User()只是一个函数,不是要实例化的类,所以它只是被正常的调用。使用new不合适,实际上也是浪费资源。

执行User()创建了一个User模块的实例 — 创建了一个全新的作用域,从而创建每个内部变量/函数的全新副本。我们将此实例分配给fred。如果我们再次运行User(),我们将获得一个完全独立于fred的新实例。

内部doLogin()函数具有usernamepassword的闭包,这意味着即使在User()函数完成运行后它也将保留对它们的访问。

publicAPI是一个对象,上面有一个属性/方法,login,它是对内部doLogin()函数的引用。当我们从User()返回publicAPI,它成为我们称之为fred的实例。

此时,外部User()函数已完成执行。通常情况下,你会认为像usernamepassword这样的内部变量已经消失了。但是在这里他们没有,因为login()函数中有一个闭包来保持它们的存活。

这就是为什么我们可以调用fred.login() - 与调用内部doLogin()相同 - 它仍然可以访问usernamepassword内部变量。

有一个很好的机会,只要简单地了解关闭和模块模式,其中一些仍然有点令人困惑。没关系!将大脑包裹起来需要一些工作。

仅仅通过对闭包和模块模式的简短了解,还不够,其中一些仍然可能感觉有点困惑。没关系!想要把它装进你的大脑确实需要做一些工作。

注意: 从这里开始,阅读本系列的Scope&Closures,进行更深入的探索。

this Identifier

JavaScript中另一个经常被误解的概念就是this标识符。同样,在本系列的this&Object Prototypes 中有几章可以讨论它,所以我们在这里简单介绍一下这个概念。

虽然看起来this似乎与“面向对象的模式”有关,但在JS中this是一种不同的机制。

如果函数在其中包含this引用,则this引用通常指向object。但它指向的object取决于函数的调用方式。

重要的是要意识到this并不是指函数本身,这是最常见的误解。

这是一个快速说明:

function foo() {
    console.log( this.bar );
}

var bar = "global";

var obj1 = {
    bar: "obj1",
    foo: foo
};

var obj2 = {
    bar: "obj2"
};

// --------

foo();                // "global"
obj1.foo();            // "obj1"
foo.call( obj2 );        // "obj2"
new foo();            // undefined

如何设置它有四个规则,它们显示在该片段的最后四行中。

  1. foo()最终以非严格模式将this设置为全局对象 - 在严格模式下,this将是undefined的,并且在访问bar属性时会出错 - 因此,在此处,"global"this.bar此找到的值。

  2. obj1.foo()设置thisobj1对象。

  3. foo.call(obj2) 设置 this为obj2` 对象。

  4. new foo()设置this为一个全新的空对象。

底线:要理解this指向,你必须检查所讨论的函数是如何被调用的。这将是刚刚展示的四种方式中的一种,然后它将回答this是什么。

注意: 有关this内容的更多信息,请参阅本系列的 this&Object Prototypes 的第1章和第2章。

Prototypes

JavaScript中的原型机制非常复杂。我们只会在这里看一眼。将需要花费大量时间来查看本系列的this & Object Prototypes 的第4-6章,以获取所有细节信息。

当你引用对象上的属性时,如果该属性不存在,JavaScript将自动使用该对象的内部原型引用来查找另一个对象以查找该属性。如果属性丢失,你几乎可以认为这是一个后备。

从一个对象到其回退的内部原型引用链接在创建该对象时发生。说明它的最简单方法是使用一个名为Object.create()的内置方法。

考虑下面代码:

var foo = {
    a: 42
};

// create `bar` and link it to `foo`
var bar = Object.create( foo );

bar.b = "hello world";

bar.b;        // "hello world"
bar.a;        // 42 <-- delegated to `foo`

它可能有助于可视化foobar对象及其关系:

a属性并不是真实的存在bar对象上,但由于bar是原型链接到foo,因此JavaScript会自动回退到foo对象上查找a,并在那里找到他。

这种联系似乎是语言的一个奇怪特征。使用这个特性的最常见方式——我认为,是滥用 ——是试图用“继承”来模拟/伪造一个“类”机制。

但是一种更自然的应用原型的方法是一种称为“行为委托”的模式,在这种模式中,你有意设计链接对象,以便能够从一个委托到另一个委托,以获得所需行为的部分内容。

注意: 有关原型和行为委派的更多信息,请参阅本系列的 this &Object Prototypes 第4-6章。

Old & New

我们已经介绍过的一些JS特性,以及本系列其他部分所介绍的许多特性,都是新增功能,并不一定适用于旧版浏览器。事实上,规范中的一些最新功能甚至还没有在任何稳定的浏览器中实现。

那么,你怎么处理这些新东西呢?你只需要等上几年或几十年,所有的旧浏览器就会逐渐消失?

这是许多人对这种情况的看法,但这对JS来说真的不是一种健康的方法。

你可以使用两种主要技术将较新的JavaScript内容“引入”旧版浏览器:填充(polyfilling)和转译(transpiling)。

Polyfilling

“polyfill”是一个发明的术语(由remy sharpl发明)(https://remysharp.com/2010/10/08/what-is-a-polyfill),用于指获取新特性的定义并生成与行为等效的代码,但能够在旧的JS环境中运行。

例如,ES6定义了一个名为Number.isNaN()的实用程序,以便为NaN值提供准确无误的检查,并弃用原始的isNaN()实用程序。但是很容易填充该实用程序,以便可以在代码中开始使用它,无论最终用户是否在ES6浏览器中。

考虑下面代码:

if (!Number.isNaN) {
    Number.isNaN = function isNaN(x) {
        return x !== x;
    };
}

if语句防止在已存在的ES6浏览器中应用填充定义。如果它尚未存在,我们定义Number.isNaN()

注意: 我们在这里做的检查利用了NaN值具有的怪异行为,它们是整个语言中唯一不等于它自身的值。所以NaN值是唯一能使x !== xtrue的值。

并非所有新功能都是完全可填充的。有时,大多数行为都可以进行多层填充,但仍然存在小的偏差。你应该非常,非常小心地自己实施填充,以确保你尽可能严格遵守规范。

或者更好的是,使用已经经过审查的一组你可以信任的填充,例如ES5-Shim(https://github.com/es-shims/es5-shim)和ES6-Shim(https://github.com/es-shims/es6-shim)提供的那些。

Transpiling

没有办法填充已添加到该语言的新语法。新的语法会在旧的JS引擎中引发一个无法识别/无效的错误。

因此,更好的选择是使用一种工具将新代码转换为旧代码。这个过程通常被称为"转译",一个用于转换+编译的术语。

本质上,源代码是以新的语法形式编写的,但部署到浏览器的是以旧语法形式生成的代码。你通常将转译器插入到构建过程中,类似于代码linter或压缩器。

你可能会想,为什么你要费劲去写新的语法,却把它变成了旧的代码——为什么不直接写旧的代码呢?

你应该关注转译的几个重要原因:

  • 添加到该语言的新语法旨在使代码更具可读性和可维护性。较旧的等价物往往更复杂。你应该更喜欢编写更新更清晰的语法,不仅适用于你自己,也适用于开发团队的所有其他成员。

  • 如果你只针对较旧的浏览器进行转译,但是将新语法提供给最新的浏览器,则可以利用新语法优化浏览器性能。这也让浏览器制造商拥有更多真实的代码来测试他们的实现和优化。

  • 更早地使用新语法可以让它在现实世界中得到更健壮的测试,从而为JavaScript委员会(TC39)提供更早的反馈。如果及早发现问题,可以在这些语言设计错误成为永久性错误之前对其进行更改/修复。

这是一个快速的转译示例。ES6添加了一个名为“默认参数值”的功能。它看起来像这样:

function foo(a = 2) {
    console.log( a );
}

foo();        // 2
foo( 42 );    // 42

很简单,对吧?也很有用!但它的新语法在ES6之前的引擎中无效。那么转换器将如何处理该代码以使其在较旧的环境中运行?

function foo() {
    var a = arguments[0] !== (void 0) ? arguments[0] : 2;
    console.log( a );
}

如你所见,它检查arguments[0]值是否为void 0(也称为undefined),如果是,则提供2为默认值;否则,它会分配传递来的任何内容。

除了能够在更老的浏览器中使用更好的语法之外,查看转译后的代码实际上更清楚地解释了预期的行为。

你可能没有从 es6 版本中意识到, undefined是唯一不能作为 默认值参数 显式传递的值, 但转换后的代码使这一点更加清晰。

强调转换器的最后一个重要细节是它们现在应该被认为是JS开发生态系统和过程的标准部分。JS将比以前更快地继续发展,因此每隔几个月就会添加新的语法和新功能。

如果你默认使用一个转换器,那么你将总是可以在发现新语法有用时,立即开始使用它,而不是总是等待多年才能让今天的浏览器逐步淘汰。

有很多很棒的编译器供你选择。在撰写本文时,这里有一些很好的选择:

Non-JavaScript

到目前为止,我们所涵盖的唯一内容是JS语言本身。现实情况是,大多数JS都是为了运行并与浏览器等环境进行交互而编写的。严格来说,你在代码中编写的大部分内容都不是由JavaScript直接控制的。这可能听起来有点奇怪。

你将遇到的最常见的非JavaScript JavaScript是DOM API。例如:

var el = document.getElementById( "foo" );

当你的代码在浏览器中运行时,document变量作为全局变量存在。它不是由JS引擎提供的,也不是由JavaScript规范控制的。它采用的形式看起来很像普通的JS对象,但实际上并非如此。它是一个特殊的object,通常称为"宿主对象"。

此外,document上的getElementById()方法看起来像普通的JS函数,但它只是一个由浏览器中的DOM提供的内置方法的一个接口。在一些(新一代)浏览器中,这层也可能在JS中,但传统上DOM及其行为是在更像C/C++的实现中实现的。

另一个例子是输入/输出(I/O)。

每个人最喜欢的alert(..)会在用户的浏览器窗口中弹出一个消息框。alert(..)由浏览器提供给你的JS程序,而不是由JS引擎本身提供。你所做的调用将消息发送到浏览器内部,它处理绘图和显示消息框。

console.log(..)也是同样的道理;你的浏览器提供了这样的机制,并将它们与开发人员工具联系起来。

本书和整个系列专注于JavaScript语言。这就是为什么你没有看到这些非JavaScript JavaScript机制的任何实质性报道。然而,你需要了解它们,因为它们将出现在你编写的每个JS程序中!

Review

学习JavaScript编程风格的第一步是基本了解其核心机制,如值,类型,函数闭包,this和原型。

当然,这些主题中的每一个都应该得到比你在这里看到的更多的报道,但这就是为什么他们在本系列的其余部分中都有专门的章节和书籍。在你对本章中的概念和代码示例感到非常满意之后,本系列的其余部分将等待你深入挖掘并深入了解该语言。

本书的最后一章将简要概述本系列中的其他每个标题以及它们涵盖的其他概念,以及我们已经探索过的内容。

Last updated