你不知道的JavaScript · 摘要

1、JavaScript 是一门编译语言。但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。尽管如此,JavaScript 引擎进行编译的步骤和传统的编译语言非常相似,在某些环节可能比预想的要复杂。

2、在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。

  1. 分词/词法分析;这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。
  2. 解析/语法分析;这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
  3. 代码生成。将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。

3、比起那些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。

4、首先,JavaScript 引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化,因为与其他语言不同,JavaScript 的编译过程不是发生在构建之前的。

对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种办法(比如 JIT,可以延迟编译甚至实施重编译)来保证性能最佳。

5、变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

6、当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。
讲得更准确一点,RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。从这个角度说,RHS 并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”,可以理解为“得到某某的值”。

7、作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。

不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)。

8、词法作用域查找只会查找一级标识符,比如 a 、b 和 c 。如果代码中引用了 foo.bar.baz ,词法作用域查找只会试图查找 foo 标识符,找到这个变量后,对象属性访问规则会分别接管对 bar 和 baz 属性的访问。

9、JavaScript 中有两个机制可以“欺骗”词法作用域: eval(..) 和 with 。前者可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

10、对函数的传统认知就是先声明一个函数,然后再向里面添加代码。但反过来想也可以带来一些启示:从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”起来了。

11、在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。

这个原则可以延伸到如何选择作用域来包含变量和函数。如果所有变量和函数都在全局作用域中,当然可以在所有的内部嵌套作用域中访问到它们。但这样会破坏前面提到的最小特权原则,因为可能会暴漏过多的变量或函数,而这些变量或函数本应该是私有的,正确的代码应该是可以阻止对这些变量或函数进行访问的。

12、规避冲突的方法。

  1. 全局命名空间;通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中。
  2. 模块管理。从众多模块管理器中挑选一个来使用。

13、函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。

区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

第一个片段中 foo 被绑定在所在作用域中,可以直接通过foo() 来调用它。第二个片段中 foo 被绑定在函数表达式自身的函数中而不是所在作用域中。
换句话说, (function foo(){ .. }) 作为函数表达式意味着 foo 只能在 .. 所代表的位置中被访问,外部作用域则不行。 foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。

14、函数表达式可以是匿名的,而函数声明则不可以省略函数名——在 JavaScript 的语法中这是非法的。

匿名函数表达式书写起来简单快捷,很多库和工具也倾向鼓励使用这种风格的代码。但是它也有几个缺点需要考虑。

  1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
  3. 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

行内函数表达式非常强大且有用——匿名和具名之间的区别并不会对这点有任何影响。给函数表达式指定一个函数名可以有效解决以上问题。始终给函数表达式命名是一个最佳实践

setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
    console.log( "I waited 1 second!" );
}, 1000 );

15、考虑以下代码:

a = 2;
var a;
console.log( a );

你认为 console.log(..) 声明会输出什么呢?
很多开发者会认为是 undefined ,因为 var a 声明在 a = 2 之后,他们自然而然地认为变量被重新赋值了,因此会被赋予默认值 undefined 。但是,真正的输出结果是 2 。

考虑另外一段代码:

console.log( a );
var a = 2;

鉴于上一个代码片段所表现出来的某种非自上而下的行为特点,你可能会认为这个代码片段也会有同样的行为而输出 2 。还有人可能会认为,由于变量 a 在使用前没有先进行声明,因此会抛出 ReferenceError 异常。
不幸的是两种猜测都是不对的。输出来的会是 undefined 。

16、函数声明会被提升,但是函数表达式却不会被提升。

foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
    // ...
};
// 这段程序中的变量标识符 foo() 被提升并分配给所在作用域(在这里是全局作用域),因此foo() 不会导致 ReferenceError 。但是 foo 此时并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值)。 foo() 由于对 undefined 值进行函数调用而导致非法操作,因此抛出 TypeError 异常。

同时也要记住,即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用

foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
    // ...
};

17、函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个“重复”声明的代码中)是函数会首先被提升,然后才是变量

foo(); // 1
var foo;
function foo() {
    console.log( 1 );
}
foo = function() {
    console.log( 2 );
};

18、一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:

foo(); // "b"
var a = true;
if (a) {
    function foo() { console.log("a"); }
} else {
    function foo() { console.log("b"); }
}

但是需要注意这个行为并不可靠,在 JavaScript 未来的版本中有可能发生改变,因此应该尽可能避免在块内部声明函数

19、无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

20、在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

21、尽管 IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用闭包。

22、延迟函数的回调会在循环结束时才执行。

// 该循环输出结果为:5个6
for (var i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。 使用如下方法可得到正确的结果。

for (var i=1; i<=5; i++) {
    (function(j) {
        setTimeout( function timer() {
            console.log( j );
        }, j*1000 );
    })( i );
}

我们使用 IIFE 在每次迭代时都创建一个新的作用域。换句话说,每次迭代我们都需要一个块作用域。使用 let 声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

for (let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

23、模块模式需要具备两个必要条件。
1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

24、模块模式另一个简单但强大的变化用法是,命名将要作为公共 API 返回的对象:

var foo = (function CoolModule(id) {
    function change() {
        // 修改公共 API
        publicAPI.identify = identify2;
    }
    function identify1() {
        console.log( id );
    }
    function identify2() {
        console.log( id.toUpperCase() );
    }
    var publicAPI = {
        change: change,
        identify: identify1
    };
    return publicAPI;
})( "foo module" );
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

通过在模块实例的内部保留对公共 API 对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。

25、词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。( this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。 Javascript是词法作用域。

26、需要明确的是, this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript代码访问,它存在于 JavaScript 引擎内部。

27、学习 this 的第一步是明白 this 既不指向函数自身也不指向函数的词法作用域,你也许被这样的解释误导过,但其实它们都是错误的。
this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

28、虽然 this 的绑定规则完全取决于调用位置,但是只有 函数 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 函数的调用位置无关。

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。

29、

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值