你不知道的javaScript(上)

第1章 作用域是什么

几乎所有编程语言最基本的功能之一,是能够储存变量中的值,并能在之后对这个值进行访问或修改。

这种储存和访问变量的值的能力将状态带给了程序。

将变量引入程序要讨论几个问题:这些变量住在哪里?储存在哪里?程序需要时如何找到它们?

需要一套设计良好的规则来存储变量,之后可以方便找到这些变量,这套规则被称为作用域

1.1 编译原理

尽管通常将JavaScript归类为"动态"或"解释执行"语言,事实上它是一门编译语言。与传统的编译语言不同,它不是提前编译,编译结果也不能在分布式系统中进行移植。

程序中的一段源代码在执行之前会经历三个步骤,统称为"编译"

  • 分词/词法分析:将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元
  • 解析/语法分析:将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树
  • 代码生成:用来创建一个叫作a的变量(包括分配内存等),并将一个值储存在a中

1.2 理解作用域

1.2.1 演员表

  • 引擎: 从头到尾负责整个JavaScript程序的编译及执行过程
  • 编译器: 负责语法分析及代码生成等
  • 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限

1.2.2 对话

下面将var a = 2 ;分解,看看引擎和它的朋友们是如何协同工作的

  1. 遇到 var a ,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。若是,编译器会忽略该声明,继续进行编译;否则会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a

  2. 编译器会为引擎生成运行时所需代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。若是,引擎会使用这个变量,否,引擎会继续查找该变量

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

1.2.3 编译器有话说

引擎会为变量进行LHS查询。另一个查找的类型叫作RHS

当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询

RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量的容器本身,可以对其赋值,RHS并不是真正意义上的"赋值操作的右侧",更准确地说是"非左侧"

1.3 作用域嵌套

作用域是根据名称查找变量的一套规则。

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。

在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域为止

1.4 异常

严格模式在行为上有很多不同,一个不同的行为是严格模式禁止自动或隐式地创建全局变量

ReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功了,但是对结果的操作是非法或不合理的

不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式)

第2章 词法作用域

作用域有两种工作模型,词法作用域和动态作用域

2.1 词法阶段

词法作用域是定义在词法阶段的作用域

function foo(a){
  var b = a * 2;
  function bar(c) {
    console.log(a, b, c);
  }
  bar(b * 3);
}
foo(2);       // 2, 4, 12

在这个例子中有3个逐级嵌套的作用域

  • 包含整个全局作用域,其中有一个标识符:foo
  • 包含着foo所创建的作用域,其中有三个标识符:a、bar 和 b
  • 包含着bar所创建的作用域,其中只有一个标识符:c

查找

在多层嵌套作用域中可以定义同名的标识符,叫作"遮蔽效应"。

抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者向上进行,直到遇见第一个匹配的标识符为止

全局变量会自动成为全局对象的属性(window.a),通过这种技术可以访问被同名变量所遮蔽的全局变量

2.2 欺骗词法

eval

eval(..)函数可以接受一个字符串为参数

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

with

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

var obj = {
  a: 1,
  b: 2,
  c: 3
}
//单调乏味的重复"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
//简单的快捷方式
with(obj){
  a = 3;
  b = 4;
  c = 5;
}

还会泄漏到全局作用域上

function foo(obj){
    with(obj){
    a = 2;
  }
}
var o1 = {
  a: 3
};
var o2 = {
  b: 3
}
foo(o1);
console.log(o1.a);    // 2

foo(o2);
console.log(o2.a);    // undefined
console.log(a);       // 2 ————不好,a被泄漏到全局作用域上了

不推荐使用eval(..)和with的原因是会被严格模式所影响(限制)。with被完全禁止,eval(..)也被间接禁止了

前者可以对一段包含一个或多个声明的"代码"字符串进行演算,并借此来修改已经存在的词法作用域。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,创建一个新的词法作用域

第3章 函数作用域和块作用域

3.1 函数中的作用域

基于函数的作用域,每声明一个函数都会为其自身创建一个气泡,而其他结构都不会创建作用域气泡。

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用。

3.2 隐藏内部实现

变量或函数本应该是私有的,正确代码应该是可以阻止对这些变量或函数进行访问

function doSomething(a){
  function doSomethingElse(a){
    return a - 1;
  }
  var b;
  b = a + doSomethingElse(a * 2);
  console.log(b * 3);
}
doSomething(2);     // 15

规避冲突

"隐藏"作用域中的变量和函数所带来的另一个好处,可以避免同名标识符之间的冲突

function foo(){
  function bar(a){
    i = 3;  // 修改for循环所属作用域中的i
    console.log(a + i);
  }
  for(var i = 0; i < 10; i++){
    bar(i * 2); // 糟糕,无限循环了!
  }
}
foo();

1.全局命名空间

程序加载第三方库时,若没有妥善将内部私有的函数或变量隐藏起来,会引发冲突。

这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。此对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,不是将自己的标识符暴露在顶级的词法作用域中。

var MyReallyCoolLibrary = {
  awesome: "stuff",
  doSomething: function(){
    //...
  },
  doAnotherThing: function(){
    //...
  }
}

2.模块管理

3.3 函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义"隐藏"起来,外部作用域无法访问包装函数内部的任何内容。

var a = 2;
function foo(){ // <-- 添加这一行
  var a = 3;
  console.log(a);  // 3
}  // <-- 以及这一行
foo();  // <-- 以及这一行
console.log(a);   // 2

函数会被当作函数表达式而不是一个标准的函数声明来处理

3.3.1 匿名和具名

1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难

2.若没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用

3.匿名函数省略了对于代码可读性/可理解性很重要的函数名

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

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

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

由于函数被包含在一对()括号内部,成为一个表达式,第一个()将函数变成表达式,第二个()执行了这个函数

另一种改进形式:(function(){..}()),用来调用的()括号被移进了用来

你可能感兴趣的:(你不知道的javaScript(上))