Skip to main content

你不知道的JavaScript(1)

1.作用域#

javascript是一门编译语言,与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中移植。

编译的三个步骤#

1. 分词,词法分析#

将字符串分解为有意义的代码块,这些代码块被称为词法单元。

2. 解析#

将词法单元流转换成一个由数组逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”

3. 代码生成#

将AST转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关。

对于javascript来说,大部分情况下编译发生在代码执行前的几微秒。#

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


当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量。

异常#

LHS查询和RHS查询是有区别的

  • 如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。
  • 当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,,并将其返还给引擎,前提是程序运行在非严格模式下。
  • 严格模式下,LHS查询失败时,并不会创建并返回一个全局变量,引擎会同样抛出ReferenceError异常。

如果RHS查询到了一个变量,但是尝试对这个变量值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或者引用null或者undefined类型的值中的属性,那么引擎会抛出TypeError类型的异常。

总结#

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

2.词法作用域#

全局变量会自动成为全局对象(比如浏览器中的window对象)的属性,因此可以不直接通过全局对象的词法名称,而是间接的通过对全局对象的属性的引用来对其进行访问。比如window.a 通过这种技术可以访问那些被同名变量所遮盖的全局变量。但是非全局的变量如果被遮蔽了,无论如何都无法被访问到。

欺骗词法#

(1)eval#

JavaScript中的eval(...)函数可以接收一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。即:可以在你写的代码中用程序生成代码,并运行,就好像代码是写在那个位置的一样。

function foo(str, a) {    eval(str);  //此处就是欺骗    console.log(a,b);}var b = 2;foo("var b = 3", 1);    // 1, 3

eval(...)调用中的var b = 3这段代码会被当做本来就在那里一样来处理。由于那段代码声明了一个新的变量b,因此它对已经存在的foo(...)的词法作用域进行了修改。
在严格模式中,eval(...)在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。
在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上损失。

(2)with#

with通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

var obj = {    a: 1,    b: 2,    c: 3}//way1:obj.a = 2;obj.b = 3;obj.c = 4;//way2:with(obj) {    a = 2;    b = 3;    c = 4;}

with块可以将一个对象处理为词法作用域,但是这个块内部正常的var声明并不会被限制在这个块的作用域中,而是被添加到with所处的函数作用域中。
eval(...)with会被严格模式所影响,with被完全禁止,间接或非安全的使用eval(...)也被禁止了。

小结#

词法作用域意味着作用域是由书写代码时,函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。 JavaScript中有两个机制可以“欺骗”词法作用域:eval(...)with。前者可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质是通过将一个对象的引用当做作用域来处理,将对象的属性当做作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。 这个两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎的认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。因此不要使用他们!!

3.函数作用域和块作用#

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的,能充分利用JavaScript变量可以根据需要改变值类型的“动态”特性。
tips:从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”起来了。
最小特权原则(最小授权最小暴露原则):这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。

规避冲突#

“隐藏作用域”中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但用途不一样,无意间可能造成命名冲突。冲突会导致变量的值被意外覆盖。

全局命名空间:#

当程序中加载了多个第三方库时,如果它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突。这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中。

函数声明与函数表达式#

区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置,(不仅仅是一行代码,而是整个声明中位置)。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
函数声明和函数表达式之间最重要的区别是他们的名称标识符将会绑定在何处。 函数表达式可以是匿名的,而函数声明则不可以省略函数名——在JavaScript的语法中这是非法的。
匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。

立即执行函数表达式(IIFE):

1. (function foo() { .. })()2. (function foo() { .. }())

IIFE一个非常普遍的进阶用法是把它们当做函数调用并传递参数进去。

var a = 2;(function IIFE( global ) {    var a = 3;    console.log( a );           //3    console.log( global.a );    //2})( window );
有趣的undefinedundefined = true;(function IIFE(undefined) {    var a;    if(a === undefined) {        concole.log("undefined 在这里不是true哦");    }})();

方法论=建模=从各个维度分析,一件事情,一个问题,从不同的角度去分析。