js 深度学习 -作用域

js 深度学习 -作用域

=

You-Dont-Know-JS(你不知道的 js 这本书的开源版本)

github 国内翻译

https://github.com/JoeHetfield/You-Dont-Know-JS

掘金中文

https://juejin.cn/post/6844903478813261831

github 原帖

https://github.com/getify/You-Dont-Know-JS

散点知识

字面量: 就是值 比如 ‘bzj’ 123 等等

url 中输入: about:blank 打开空白页 方便 console 调试

任何不在”falsy”列表中的值都自动是“truthy”

控制台中 shift 键和 Enter 一起使用可以进行换行

“use strict”; 可以全局开启,也可以在某函数中开启。

undefined 还可以写为 void 0


Truthy 与 Falsy

在第一章中,我们简要地提到了值的“truthy”和“falsy”性质:当一个非boolean值被强制转换为一个boolean时,它是变成true还是false

在 JavaScript 中“falsy”的明确列表如下:

  • “” (空字符串)
  • 0, -0, NaN (非法的number
  • null, undefined
  • false

任何不在这个“falsy”列表中的值都是“truthy”。这是其中的一些例子:

  • “hello”
  • 42
  • true
  • [ ], [ 1, “2”, 3 ] (数组)
  • { }, { a: 42 } (对象)
  • function foo() { .. } (函数)

所谓自执行函数,跟普通函数一样,执行的关键是靠 ()

把函数体用 小括号括住 就相当是一个 函数名一般了。

function foo() { .. }

// foo 是函数引用表达式,然后用()执行它

foo();

// IIFE 是函数表达式,然后用()执行它

(function IIFE(){ .. })();

=

浏览器兼容的两种手段

填补 (polyfill)

多使用 if 条件判断 用老语法替换 新的在老浏览器中不合法的语法

“填补(Polyfilling)”是一个人为发明的词(由 Remy Sharp 创造)(https://remysharp.com/2010/10/08/what-is-a-polyfill)。它是指拿来一个新特性的定义并制造一段行为等价的代码,但是这段代码可以运行在老版本的 JS 环境中。

转译 (balel)

老浏览器中完全没有替代语法时,需要深度转义,就是用过时的 es 语法实现,现代语法。

没有任何办法可以填补语言中新增加的语法。在老版本的 JS 引擎中新的语法将因为不可识别/不合法而抛出一个错误。

所以更好的选择是使用一个工具将你的新版本代码转换为等价的老版本代码。这个处理通常被称为“转译(transpiling)”,表示转换 + 编译。

作用域

词法作用域 是一组关于 引擎 如何查询变量和它在何处能够找到变量的

规则。词法作用域的关键性质是,它是在代码编写时被定义的

(假定你不使用 eval() 或 with 作弊的话)。

词法作用域是编写时的,而动态作用域(和 this)是运行时的。

词法作用域关心的是 函数在何处被声明,但是动态作用域关心的是函数

从何处被(谁)调用。

赋值的目标(LHS)”和“赋值的源(RHS)

ReferenceError 是关于 作用域 解析失败的,而 TypeError 暗示着 作用域 解析成功了,但是试图对这个结果进行了一个非法/不可能的动作。

LHS 和 RHS 引用查询都从当前执行中的 作用域 开始,如果有需要(也就是,它们在这里没能找到它们要找的东西),它们会在嵌套的 作用域 中一路向上,一次一个作用域(层)地查找这个标识符,直到它们到达全局作用域(顶层)并停止,既可能找到也可能没找到。

未被满足的 RHS 引用会导致 ReferenceError **被抛出。**未被满足的 LHS 引用会导致一个自动的,隐含地创建的同名全局变量**(如果不是“Strict 模式”****),或者一个** ReferenceError**(如果是“Strict 模式”**)。

遍历嵌套 作用域 的简单规则:引擎 从当前执行的 作用域 开始,在那里查找变量,如果没有找到,就向上走一级继续查找,如此类推。如果到了最外层的全局作用域,那么查找就会停止,无论它是否找到了变量。

建筑的隐喻

为了将嵌套 作用域 解析的过程可视化,我想让你考虑一下这个高层建筑。

就像微波炉一样, 从最中心开始加热,从中心开始寻找作用域。一直到最外层彻底熟透。

你通过在你当前的楼层中查找来解析 LHS 和 RHS 引用,如果你没有找到它,就坐电梯到上一层楼,在那里寻找,然后再上一层,如此类推。一旦你到了顶层(全局 作用域),你要么找到了你想要的东西,要么没有。但是不管怎样你都不得不停止了。

欺骗词法作用域

** **标识符: js 编译器 标记作用域中变量或函数的声明的位置**(****甚至可以懒思考为 变量,函数的名称**

词法作用域意味着作用域是由编写时函数被声明的位置的决策定义的。编译器的词法分析阶段实质上可以知道所有的标识符是在哪里和如何声明的,并如此在执行期间预测它们将如何被查询。

在 JavaScript 中有两种机制可以“欺骗”词法作用域:eval(..) with。前者可以通过对一个拥有一个或多个声明的“代码”字符串进行求值,来(在运行时)修改现存的词法作用域。后者实质上是通过将一个对象引用看作一个“作用域”并将这个对象的属性看作作用域中的标识符,(同样,也是在运行时)创建一个全新的词法作用域。

1. eval(..)

动态生产代码块

2. with

修改对象属性

当我们传入 o1 with 语句声明的“作用域”就是 o1,而且这个“作用域”拥有一个对应于 o1.a 属性的“标识符”。但当我们使用 o2 作为“作用域”时,它里面没有这样的 a “标识符”,于是 LHS 标识符查询(见上面)的普通规则发生了。

反正使用 evel() 和 with 很影响性能就对了。

     但如果 引擎 在代码中发现一个 eval(..) with,它实质上就不得不 假定 自己知道的所有的标识符的位置可能是无效的,因为它不可能在词法分析时就知道你将会向eval(..)传递什么样的代码来修改词法作用域,或者你可能会向with传递的对象有什么样的内容来创建一个新的将被查询的词法作用域。

     换句话说,悲观地看,如果 eval(..) with 出现,那么它 做的几乎所有的优化都会变得没有意义,所以它就会简单地根本不做任何优化。

函数作用域

注意: 区分声明与表达式的最简单的方法是,这个语句中(不仅仅是一行,而是一个独立的语句)“function”一词的位置。如果“function”是这个语句中的第一个东西,那么它就是一个函数声明。否则,它就是一个函数表达式。

块儿作用域

1. with

我们在第二章中学习了 with。虽然它是一个使人皱眉头的结构,但它确实是一个(一种形式的)块儿作用域的例子,它从对象中创建的作用域仅存在于这个 with 语句的生命周期中,而不再外围作用域中。

2. try/catch

一个鲜为人知的是事实,JavaScript 在 ES3 中明确指出在 try/catch catch 子句中声明的变量,是属于 catch 块儿的块儿作用域的。

例如:

1
2
3
4
5
6
try {
undefined(); //用非法的操作强制产生一个异常!
} catch (err) {
console.log(err); // 好用!
}
console.log(err); // ReferenceError: `err` not found

如你所见,err 仅存在于 catch 子句中,并且在你试着从其他地方引用它时抛出一个错误。

注意: 虽然这种行为已经被明确规定,而且对于几乎所有的标准 JS 环境(也许除了老 IE)来说都是成立的,但是如果你在同一个作用域中有两个或多个 catch 子句,而它们又各自用相同的标识符名称声明了它们表示错误的变量时,许多 linter 依然会报警。实际上这不是重定义,因为这些变量都安全地位于块儿作用域中,但是 linter 看起来依然会恼人地抱怨这个事实。

为了避免这些不必要的警告,一些开发者将他们的 catch 变量命名为 err1err2,等等。另一些开发者干脆关闭 linter 对重复变量名的检查。

catch 的块儿作用域性质看起来像是一个没用的,只有学院派意义的事实,但是参看附录 B 来了解更多它如何有用的信息。

3. let

变量不提升, 作用域就在当前大括号内, 如同函数一般 , 块级作用域最优解

至此,我们看到 JavaScript 仅仅有一些奇怪的小众行为暴露了块儿作用域功能。如果这就是我们拥有的一切,而且许多许多年以来这 确实就是 我们拥有的一切,那么块作用域对 JavaScript 开发者来说就不是非常有用。

幸运的是,ES6 改变了这种状态,并引入了一个新的关键字 let,作为另一种声明变量的方式伴随着 var

块儿作用域 优化垃圾回收机制

点击事件的处理器回调函数 click 根本不 需要 someReallyBigData 变量。这意味着从理论上讲,在 process(..) 运行之后,这个消耗巨大内存的数据结构可以被作为垃圾回收。然而,JS 引擎很可能(虽然这要看具体实现)将会仍然将这个结构保持一段时间,因为click函数在整个作用域上拥有一个闭包。

块儿作用域可以解决这个问题,使引擎清楚地知道它不必再保持 someReallyBigData 了:

闭包

我们代码中本身就存在不少闭包的影子,并都在发挥这你意想不到的作用。只是有点抽象罢了。

看似合理 实则是闭包的魔力

foo() 被执行之后,一般说来我们会期望 foo() 的整个内部作用域都将消失,因为我们知道 引擎 启用了 垃圾回收器 在内存不再被使用时来回收它们。因为很显然foo()的内容不再被使用了,所以看起来它们很自然地应该被认为是 消失了

难怪说闭包会影响垃圾回收机制

就是调用一个被返回的函数时,该函数依旧能访问到,它原来的作用域

闭包就是当一个函数 即使是在它的[**词法**]作用域之外被调用时,也可以记住并访问它的[**词法**]作用域。

无论我们使用什么方法将内部函数 传送 到它的词法作用域之外,它都将维护一个指向它最开始被声明时的作用域的引用,而且无论我们什么时候执行它,这个闭包就会被行使


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!