JS作用域和闭包

读《你不知道的JavaScript》,显然没有读懂TAT

JavaScript

作用域和闭包

赋值操作

当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。
讲得更准确一点,RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身。

如console.log( a );
其中对 a 的引用是一个 RHS 引用,因为这里 a 并没有赋予任何值。相应地,需要查找并取得 a 的值。

a=2,LHS,普通的赋值目的。

eval()

JavaScript 中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书
写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并
运行,就好像代码是写在那个位置的一样。
根据这个原理来理解 eval(..) ,它是如何通过代码欺骗和假装成书写时(也就是词法期)
代码就在那,来实现修改词法作用域环境的,这个原理就变得清晰易懂了。
在执行 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(..) 的词法作用域进行了修改。事实上,和前面提到的原理一样,这段代码实际上在 foo(..) 内部创建了一个变量 b ,并遮蔽了外部(全局)作用域中的同名变量。
当 console.log(..) 被执行时,会在 foo(..) 的内部同时找到 a 和 b ,但是永远也无法找到外部的 b 。因此会输出“1, 3”而不是正常情况下会输出的“1, 2”。

严格模式的程序中, eval(..) 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。

function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );

JavaScript 中 还 有 其 他 一 些 功 能 效 果 和 eval(..) 很 相 似。 setTimeout(..) 和setInterval(..) 的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的函数代码。这些功能已经过时且并不被提倡。不要使用它们!
new Function(..) 函数的行为也很类似,最后一个参数可以接受代码字符串,并将其转化为动态生成的函数(前面的参数是这个新生成的函数的形参)。这种构建函数的语法比eval(..) 略微安全一些,但也要尽量避免使用。

(大概like动态生成,方便但是有很多麻烦)

【with:先略过,有空可以仔细看一看】

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

函数作用域

匿名函数
setTimeout( function() {
console.log("I waited 1 second!");
}, 1000 );

这叫作匿名函数表达式,因为 function().. 没有名称标识符。函数表达式可以是匿名的,而函数声明则不可以省略函数名——在 JavaScript 的语法中这是非法的。

缺点:

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

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

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

立即执行函数

var a = 2;
(function IIFE() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
-----------------------------------------------
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2

编译器

提升

因此,正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个声明: var a; 和 a = 2; 。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。

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 。

只有声明本身会被提升,而赋值或其他运行逻辑会留在 原地 。如果提升改变了代码执行的顺序,会造成非常严重的破坏。
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
foo 函数的声明(这个例子还包括实际函数的隐含值)被提升了,因此第一行中的调用可以正常执行。
另外值得注意的是,每个作用域都会进行提升操作。尽管前面大部分的代码片段已经简化了(因为它们只包含全局作用域),而我们正在讨论的 foo(..) 函数自身也会在内部对 var a 进行提升(显然并不是提升到了整个程序的最上方)。因此这段代码实际上会被理解为下
面的形式:
function foo() {
var a;
console.log( a ); // undefined
a = 2;
}
foo();
可以看到,函数声明会被提升,但是函数表达式却不会被提升。

foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};

这段程序中的变量标识符 foo() 被提升并分配给所在作用域(在这里是全局作用域),因此foo() 不会导致 ReferenceError 。但是 foo 此时并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值)。 foo() 由于对 undefined 值进行函数调用而导致非法操作,
因此抛出 TypeError 异常。同时也要记住,即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用:
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
这个代码片段经过提升后,实际上会被理解为以下形式:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
}

函数优先

函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个
“重复”声明的代码中)是函数会首先被提升,然后才是变量。
考虑以下代码:
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
会输出 1 而不是 2 !这个代码片段会被引擎理解为如下形式:
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};

注意, var foo 尽管出现在 function foo()... 的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。
foo(); // 3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
}
虽然这些听起来都是些无用的学院理论,但是它说明了在同一个作用域中进行重复定义是非常糟糕的,而且经常会导致各种奇怪的问题。
一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
}

闭包

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

小结

1.函数优先一块模模糊糊

2.undefined nan理解

3.闭包。循环和闭包。作用域和闭包。

3.项目

你可能感兴趣的:(JS作用域和闭包)