JavaScript 中的作用域与闭包

目录
  • 一、JavaScript 是一门编译语言
    • 1.1 传统编译语言的编译步骤
    • 1.2 JavaScript 与传统编译语言的区别
  • 二、作用域(Scope)
    • 2.1 LHS查询 和 RHS查询
    • 2.2 作用域嵌套
    • 2.3 ReferenceError 和 TypeError
      • (1)ReferenceError
      • (2)TypeError
      • (3)ReferenceError 和 TypeError 的区别
  • 小结
  • 三、词法作用域
    • 3.1 词法阶段
    • 3.2 词法作用域 查找规则
    • 3.3 欺骗词法 —— eval、with
      • (1)eval
      • (2)with
      • (3)为什么不推荐使用 eval() 和 with
  • 四、函数作用域和块作用域
    • 4.1 函数作用域
    • 4.2 块作用域
  • 五、函数提升和变量提升
    • 5.1 变量声明提升
    • 5.2 函数声明提升
    • 5.3 声明提升注意点
  • 六、闭包
    • 6.1 例题

前言:

前几天面试中,面试官抛出一道题,问我输出结果是啥:

var arr = []
for (var i = 0; i < 3; i++) {
    arr[i] = function() {
        console.log(i);
    }
}
arr[0]()
arr[1]()
arr[2]()

无知的我脱口而出:“三个2”,面试官眉头一皱:“你再仔细看看”。哦豁,大事不妙,赶紧仔细看看,不对是三个3。面试官点了点头,心想应该是答对了。接着面试官又问我,怎么修改呢。心想这不就是闭包吗,var改let吗。接着面试官又问我其他方法呢,我说用立即执行函数。结果到了手写的时候突然懵了,背的八股文忘了,然后就尴尬了。。。
看来只背背八股文还是不行,所以今天针对这个问题,仔细学习了一下前因后果。

一、JavaScript 是一门编译语言

通常 JavaScript 被归类于“解释性语言”或“脚本语言”等,作为开发Web 页面的脚本语言而出名。但是事实上,它是一门编译语言
MDN对JavaScript的定义如下:

JavaScript (JS) 是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。
JavaScript 是一种基于原型编程、多范式的动态脚本语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。
—— MDN

1.1 传统编译语言的编译步骤

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

1.2 JavaScript 与传统编译语言的区别

(1)JavaScript 与传统编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中移植。 (2)JavaScript引擎负责整个JavaScript程序的编译及执行过程,编译器负责语法分析及代码生成等,相对于传统编译语言的编译器更加复杂(例如:在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化)
(3)大部分情况下JavaScript 编译发生在代码执行前的几微秒(甚至更短)时间内。

二、作用域(Scope)

作用域:负责收集并维护由所有标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。—— 《你不知道的JavaScript 上卷》

在了解什么是作用域前,首先来看看var a = 2;是如何进行处理的。可能大部分和我一样认为这就是一句声明,但是JavaScript认为这里面又两个完全不同的声明,一个由编译器在编译时处理,另一个由引擎在运行时处理。

  • 首先编译器会将这段程序分解成词法单元,然后将词法单元解析成一个树结构
  • 紧接进行代码生成,编译器会进行如下处理:
    • 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a。
    • 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(即在作用域链上查找)如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个异常!

2.1 LHS查询 和 RHS查询

如上例子,编译器为引擎生成了为引擎生成了运行时所需的代码后,引擎执行它时,是如何查找变量a的呢?这里就要引入LHS查询和RHS查询两个术语了。
(1)LHS 查询:试图找到变量的容器本身,从而可以对其赋值
(2)RHS 查询:查找某个变量的值
以下面程序为例,对LHS 和 RHS 做更深一步的解释:

function foo(a) {
    console.log( a ); // 2
}
foo( 2 );
  • 首先:foo() 函数的调用,需要对foo进行RHS查询,即查找 foo 的值
  • 紧接着执行foo(2)时,这里传递参数时,隐式进行了 a = 2,那么这里需要对 a 进行LHS查询,找到a后再将2赋值给a。
  • 进入foo函数内部,然后对console进行RHS查询,然后对a进行RHS查询,传递进log()。

再看个例子:

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo( 2 );

其中有:

3次 LHS 查询

  • c = ..
  • a = 2
  • b = ..

4次 RHS 查询

  • foo(..)
  • = a
  • a ..
  • .. b

2.2 作用域嵌套

作用域简单来说就是根据名称查找变量的一套规则,但实际情况中,上述的查询的可能不仅限于一个作用域。

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套
因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(即:全局作用域)为止。

function foo(a) {
    console.log( a + b );
}
var b = 2;
foo( 2 ); // 4

例如上述代码中,对b进行RHS查询时,无法在当前函数foo的作用域中完成,需要向上一级作用域查找,即在全局作用域中完成了。
LHS查询和RHS查询都会在当前执行的作用域开始查找,如果没有找到,则会向上一级查找,直到查找成功或者达到全局作用域。达到全局作用域,无论是否找到,都会停止查询过程。

2.3 ReferenceError 和 TypeError

若在任何作用域中都无法查找到变量,那么引擎就会抛出异常。但是针对LHS查询失败和RHS查询失败抛出的异常是不同的。

(1)ReferenceError

console.log(a);

上述代码在执行时,会抛出 ReferenceError 。这是因为在对 a 进行RHS查询时,是无法查找到改变量的。这是因为变量 a ”未声明“,不存在于任何作用域中。
所以,RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError异常。
相比之下,LHS查询在所有嵌套的作用域中查询不到目标变量时,全局作用域会创建一个具有该名称的变量,并将其返还给引擎。但是,如果是在”严格模式“下,引擎也会抛出 ReferenceError.

// 严格模式下
"use strict"
a = 2;  // ReferenceError
// 非严格模式下
a = 2  // 执行成功

即:

  • RHS查询失败时:引擎会抛出 ReferenceError
  • LHS查询失败时
    • 严格模式: 引擎会抛出 ReferenceError;
    • 非严格模式:全局作用域会创建一个具有该名称的变量,并将其返还给引擎

(2)TypeError

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

// 对非函数类型的值进行调用
let a = 0;
a();

// 引用undefined类型的值的属性
let b;
b.name;

(3)ReferenceError 和 TypeError 的区别

ReferenceError 表示RHS查询失败,或严格模式下的LHS查询失败
TypeError 则代表RHS查询成功了,但是对结果的操作是非法或不合理的。

小结

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

LHS 和 RHS 查询都会在当前执行作用域中开始,如果没找到,就会向上级作用域继续查找目标标识符,这样每次上升一级作用域,最后抵达全局作用域(顶层),无论找到或没找到都将停止。

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

三、词法作用域

第二节中提到作用域可以定义为一套规则,但是这套规则又是如何去定义的呢?
作用域主要有两种主要的工作模型:词法作用域 和 动态作用域,其中 JavaScript 采用的是词法作用域

3.1 词法阶段

如第一节中介绍的,大部分标准语言编译器的第一个工作阶段叫作词法分析。即对源代码中的字符进行检查,识别出每个单词。
简单来说,词法作用域就是定义在词法阶段的作用域。即由代码中变量的书写位置来决定的

3.2 词法作用域 查找规则

(1)作用域查找是找从运行时所处的最内部作用域开始,逐级向外,直到遇见第一个匹配的标识符为止。
(2)遮蔽效应:在多层嵌套的作用域中可以定义同名的标识符,但是内部的标识符会”遮蔽“外部的标识符
(3)全局变量会自动成为全局对象的属性。所以可以通过全局对象的引用来间接访问全局变量。

// a是全局变量
var a = 1;
// 浏览器中全局对象一般为window
console.log(window.a)  // 1

所以,当全局变量在内部作用域被同名变量“遮蔽”时,可通过该方法访问到全局变量,例如:

// a是全局变量
var a = 1;

funcion foo() {
    let a = 2;
    console.log(a);  // 2
    console.log(window.a);  // 1
}

但是,对于非全局变量来说,如果被遮蔽了,就无法访问到。
(4)无论函数何时、何处以及如何被调用,它的词法作用域都只由被声明时所处的位置决定。(即与代码中书写的位置保持一致)
(5)词法作用域的查找只会查找\color{red}{一级标识符}一级标识符。
例如:针对foo.a.b,词法作用域只会试图查找 foo 标识符,找到 foo 这个变量后,对象属性访问规则会接管对 a 和 b 属性的访问。
这也解释了前面当引擎遇到console.log();时,只会对 console 进行一次RHS查询,不会接着对 log 进行RHS查询。

3.3 欺骗词法 —— eval、with

3.2中说到,词法作用域是由书写代码期间函数所声明的位置来定义。但是JavaScript中有两个机制会在运行时“修改”词法作用域——eval、with。但是很多地方都建议不使用这两种机制,因为欺骗词法作用域会导致性能下降。

(1)eval

eval() 是全局对象的一个函数属性。
eval() 的参数是一个字符串。如果字符串表示的是表达式,eval() 会对表达式进行求值。如果参数表示一个或多个 JavaScript 语句,那么eval() 就会执行这些语句。 —— MDN

换个说法,eval可以在书写的代码中用程序生成代码并运行,就好像代码是写在那个位置一样。
在执行 eval(..) 之后的代码时,引擎并不“知道”或“在意”前面的代码是以动态形式插入进来,并对词法作用域的环境进行修改的。引擎只会如往常地进行词法作用域查找。

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

从上述代码可以看出,书写的代码中 foo 函数的词法作用域并没有声明变量 b。但是eval(..) 调用中的 var b = 3; 这段代码会被当作本来就在那里一样来处理。因此对foo函数的词法作用域进行了修改,在foo函数内部创建了一个变量b,遮蔽了全局变量b,所以输出 1, 3。
eval(..) 可以在运行期修改书写期的词法作用域。但个人觉得其实并没有破坏词法作用域的查找规则,即把 eval() 的参数在eval书写的位置替换eval()。然后再按词法作用域规则去查找。

\color{red}{注意:}注意:在严格模式下,eval()在运行时有自己的词法作用域,所以其中的声明无法修改所在的作用域。

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

从上述代码可以看出,严格模式下,在eval()函数内部输出b,值为3,但是在eval()函数外部输出b,值为2.

不推荐使用与 eval() 以及与 eval() 类似的函数setTimeout(..) 和setInterval(..) 的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的函数代码。这些功能已经过时且并不被提倡。(目前一般是传递回调函数)
new Function(..) 最后一个参数可以接受代码字符串,并将其转化为动态生成的函数(前面的参数是这个新生成的函数的形参)。这种构建函数的语法比eval(..) 略微安全一些,但也要尽量避免使用。

(2)with

'with'语句将某个对象添加到作用域链的顶部,如果在statement中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出ReferenceError异常。—— MDN

with (expression) {
    statement
}

换种说法,with 可以将一个没有或有多个属性的对象处理为一个完全隔离的全新的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。

var c = 3;
let obj = {
    a: 1,
    b: 2
}
with(obj) {
    console.log(a);  // 1
    var b = 5;
    console.log(b);  // 5
    console.log(c);  // 3
    console.log(d);  // ReferenceError
}

对于上述代码,我们可以这样理解,with 语句创建了一个全新的词法作用域,并把 obj 放在该词法作用域的顶层(若把该词法作用域类比为全局作用域,那么obj就是一个全局对象)。在该全新的作用域中,obj的所有属性都可以直接访问。
console.log(a)输出1:当前词法作用域未声明变量a,所以向上一级查找,obj中包含属性a,所以输出1
console.log(b)输出5:当前词法作用域中声明了变量b,该变量b”遮蔽“了obj中的属性b,所以输出5
console.log(c)输出3:当前词法作用域未定义变量c,obj中也没有属性c,则继续向全局作用域查找,所以输出3
console.log(d)抛出异常ReferenceError:因为在当前词法作用域以及其嵌套的所有词法作用域中都未声明变量d,RHS查询失败,所以抛出ReferenceError

\color{red}{注意:}注意:在 ECMAScript 5严格模式下,with标签已经被禁用。

(3)为什么不推荐使用 eval() 和 with

1. eval() 和 with 对性能的影响JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
但是当引擎在代码中遇见了 eval() 或者 with,无法直到eval()中的字符串参数如何对作用域进行修改,也不知道 with 用来创建新词法作用域的对象的内容到底是什么。因为eval() 和 with 是在运行时修改或创建新的词法作用域,所以这会影响引擎在编译阶段的性能优化,会导致程序运行变慢。
2. 严格模式下严格模式下,eval()在运行时有自己的词法作用域,而with则被禁用了。
3. eval() 函数不安全如果你用 eval() 运行的字符串代码被恶意方(不怀好意的人)修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码
4. with的弊端

  • with使用'with'可以减少不必要的指针路径解析运算。(但是很多情況下,也可以不使用with语句,而是使用一个临时变量来保存指针,来达到同样的效果)
  • with语句使得程序在查找变量值时,都是先在指定的对象中查找。所以那些本来不是这个对象的属性的变量,查找起来将会很慢
  • with语句使得代码不易阅读,同时使得JavaScript编译器难以在作用域链上查找某个变量,难以决定应该在哪个对象上来取值

四、函数作用域和块作用域

第三节中指出,词法作用域是书写代码时的位置来决定的。但是这些词法作用域时基于什么的位置来确定的呢?JavaScript中主要具有函数作用域和块作用域两种。

4.1 函数作用域

简单来说,函数作用域就是指,属于这个函数的全部变量都可以在整个函数范围内使用及复用(在嵌套的作用域中也可以使用)。但是外部作用域无法访问函数内部的任何内容。

4.2 块作用域

块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指 { .. } 内部) (1)用 with 从对象中创建出的块作用域仅在 with 声明中而非外部作用域中有效。
(2)JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。
(3)let 关键字可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。
for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值
(4)const 同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误

五、函数提升和变量提升

在介绍闭包之前,我还要啰嗦几句,以便后续更好解释例题。

5.1 变量声明提升

对于一段JavaScript代码。我们可能会认为时从上到下一行一行地去执行的,但实际上并不完全是这样的。

console.log(a);  // 1
var a = 1;

如果程序是从上到下执行的话,那么第一行代码应该会抛出ReferenceError,因为并没有在这之前并没有声明变量a。但实际上会输出 undefined ,这是为啥呢?
这要从编译开始说起了,引擎在解释JavaScript代码前会先对其进行编译,编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。所以,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理

所以上述代码实际的执行顺序是:

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

\color{red}{注意}注意:

  • 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。所以上述代码var a = 2 中只有 var a 提升了。
  • ES6中新加入的let 和 const 关键字声明变量时,并不会进行变量提升。

5.2 函数声明提升

除了变量声明会提升,函数声明也会提升。

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

如上代码,实际的执行顺序如下:

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

此外,需要注意的是,只有函数声明会提升,函数表达式并不会提升

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

5.3 声明提升注意点

函数声明先提升,然后再变量声明提升

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

一个普通块内部的函数声明通常会被提升到所在作用域的顶部

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

var 声明的是函数作用域,所以在一个普通块内部,var的变量声明也会提升

console.log(a)  // undefined
if(false) {
    var a = 1;
}
console.log(a)  // ReferenceError
function f() {
    var a = 1;
}

六、闭包

介绍完前面的知识后,终于可以引出主角闭包了,首先看看MDN中对闭包的定义:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。 —— MDN

好像有点晦涩难懂,再来看看《你不知道的JavaScript上卷》中对闭包的定义:

当函数可以记住访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 —— 《你不知道的JavaScript上卷》

还是先来看两段代码吧:

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

基于词法作用域的查找规则,函数bar()可以访问外部作用域中的变量a。这是闭包吗?反正我之前认为这就是。但是确切来说,这并不是闭包。

function foo() {
    var a = 2;
    function bar() {
        console.log( a );
    }
    return bar;
}
var baz = foo();
baz(); 

这段代码就很清晰地展示了闭包,当foo()执行完毕后,通常会销毁foo的内部作用域,但是闭包阻止了这一行为。bar()它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。
这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

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

第一段代码中,bar() 就是在其词法作用域内执行的,所以严格来说并不能称为闭包,因为并不需要“记住”词法作用域。

6.1 例题

既然了解了什么闭包,我们来看看文章开头的面试题:

var arr = []
for (var i = 0; i < 3; i++) {
    arr[i] = function() {
        console.log(i);
    }
}
arr[0]()  // 3
arr[1]()  // 3
arr[2]()  // 3

三个函数调用的结果都是3,为什么会这样呢?

首先看for循环中的var i = 0;,其中声明的变量 i 是全局作用域的一个变量,所以在执行arr[0]() 、arr[1]()、arr[2]()的时候,在作用域链上查找变量 i 时,最终找到都是全局作用域中的同一个变量 i。因为经历了三次循环,所以 i 的值变成了3。故调用三个函数输出的值都是3。
那么,怎么去改进使得程序由正确的输出呢?

1. for循环中使用 let 声明i

前面提到,for 循环头部的 let 将 i 绑定到了 for 循环的块中,指出变量 i 在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量

var arr = []
for (let i = 0; i < 3; i++) {
    arr[i] = function() {
        console.log(i);
    }
}
arr[0]()  // 0
arr[1]()  // 1
arr[2]()  // 2

2. 立即执行函数(IIFE)

var arr = []
for (var i = 0; i < 3; i++) {
    (function IIFE(i) {
        arr[i] = function() {
            console.log(i);
        }
    })(i)
}
arr[0]()  // 0
arr[1]()  // 1
arr[2]()  // 2

此外这里再分析一种错误的写法:

var arr = []
for (var i = 0; i < 3; i++) {
    var j = i;
    arr[i] = function() {
        console.log(j);
    }
}
arr[0]()  // 2
arr[1]()  // 2
arr[2]()  // 2

这也是我改进的最初答案,想着使用一个变量记录当前的i值不就行了。但是结果并不像我想的那样,翻阅书籍后,发现var声明的作用域是函数作用域,所以在for循环块中的var j = i也会声明提升。相当于j也是一个全局变量了。最后三个函数中查找到的j也相同。

到此这篇关于JavaScript 中的作用域与闭包的文章就介绍到这了,更多相关JS作用域与闭包内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解JavaScript作用域、作用域链和闭包的用法

    1. 作用域 作用域是指可访问的变量和函数的集合. 作用域可分为全局作用域和局部作用域. 1.1 全局作用域 全局作用域是指最外层函数外面定义的变量和函数的集合. 换言之,这些最外层函数外面定义的变量和函数在任何地方都能访问. 举个例子: // 最外层定义变量 var a = 1; console.log(a); // 最外层可以访问 function fnOne() { // 最外层函数 console.log(a); // 函数内可以访问 function fnTwo() { // 子函数

  • 详解JavaScript作用域 闭包

    JavaScript闭包,是JS开发工程师必须深入了解的知识.3月份自己曾撰写博客<JavaScript闭包>,博客中只是简单阐述了闭包的工作过程和列举了几个示例,并没有去刨根问底,将其弄明白! 现在随着对JavaScript更深入的了解,也刚读完<你不知道的JavaScript(上卷)>这本书,所以乘机整理下,从底层和原理上去刨一下. JavaScript并不具有动态作用域,它只有词法作用域.词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的.了解闭包前,首先我

  • JavaScript使用闭包模仿块级作用域操作示例

    本文实例讲述了JavaScript使用闭包模仿块级作用域操作.分享给大家供大家参考,具体如下: 在阅读这篇文章之前,建议先阅读JavaScript的作用域链以及JavaScript闭包. 正如闭包的定义一样:"闭包指的是有权访问另一个函数作用域中的变量的函数", 闭包最大的意义就在于闭包可以对另一个函数作用域的变量进行访问,由此,闭包可以延伸出一系列的用法. 模仿块级作用域 JavaScript没有块级作用域的概念.这意味着在块语句中定义的变量,实际上是包含在函数中而非语句中创建的.从

  • JS页面获取 session 值,作用域和闭包学习笔记

    本文实例讲述了JS页面获取 session 值,作用域和闭包.分享给大家供大家参考,具体如下: Javascript获取session的值: var name= "${sessioScope.变量名}"; 注意这里面需要使用 "" 把 El 表达式给括起来,否则就取不到数据. JSP获取session的值: 可以直接${sessionScope.变量名},在标签里也是一样. JSP获取URL的值: var name = "<%=request.get

  • JavaScript 函数用法详解【函数定义、参数、绑定、作用域、闭包等】

    本文实例讲述了JavaScript 函数用法.分享给大家供大家参考,具体如下: 初始函数 Function类型,即函数的类型. 典型的JavaScript函数定义: function 函数名称(参数表){ //函数执行部分 return ; } //注意:参数列表直接写形参名即可 return语句:return返回函数的返回值并结束函数运行 函数也可以看做数据来进行传递 参数列表相当于函数入口,return 语句相当于函数出口 函数可以作为参数来传递. function test ( a ) {

  • JS难点同步异步和作用域与闭包及原型和原型链详解

    目录 JS三座大山 同步异步 同步异步区别 作用域.闭包 函数作用域链 块作用域 闭包 闭包解决用var导致下标错误的问题 投票机 闭包两个面试题 原型.原型链 原型对象 原型链 完整原型链图 JS三座大山 同步异步 前端中只有两个操作是异步的: 定时器异步执行; ajax异步请求 编译器解析+执行代码原理: 1.编译器从上往下逐一解析代码 2.判断代码是同步还是异步 同步:立即执行 异步:不执行.放入事件队列池 3.等所有同步执行完毕开始执行异步 同步异步区别 api : 异步有回调,同步没有

  • Javascript作用域与闭包详情

    目录 1.作用域 2.作用域链 3.词法作用域 5.闭包的应用 6.闭包的缺陷 7.高频闭包面试题 1.作用域 简单来说,作用域是指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限 在ES5中,一般只有两种作用域类型: 全局作用域:全局作用域作为程序的最外层作用域,一直存在 函数作用域:函数作用域只有在函数被定义时才会被创建,包含在父级函数作用域或全局作用域中 说完概念,我们来看下面这段代码: var a = 100 function test(){ var b = a * 2 var

  • 深入了解JS之作用域和闭包

    作用域和闭包 ECMAScript5: JS 的代码没有代码块:使用函数运行的机制进行创建闭包:闭包就是作用域的意思: ES5中,JS中只有函数才可以创建能操作的作用域: JavaScript中的内存也分为栈内存和堆内存.一般来说,栈内存中存放的是存储对象的地址,而堆内存中存放的是存储对象的具体内容.对于原始类型的值而言,其地址和具体内容都存在与栈内存中:而基于引用类型的值,其地址存在栈内存,其具体内容存在堆内存中.堆内存与栈内存是有区别的,栈内存运行效率比堆内存高,空间相对推内存来说较小,反之

  • JavaScript 中的作用域与闭包

    目录 一.JavaScript 是一门编译语言 1.1 传统编译语言的编译步骤 1.2 JavaScript 与传统编译语言的区别 二.作用域(Scope) 2.1 LHS查询 和 RHS查询 2.2 作用域嵌套 2.3 ReferenceError 和 TypeError (1)ReferenceError (2)TypeError (3)ReferenceError 和 TypeError 的区别 小结 三.词法作用域 3.1 词法阶段 3.2 词法作用域 查找规则 3.3 欺骗词法 ——

  • javascript中的作用域和闭包详解

    一.JavaScript作用域 JavaScript变量实际上只有两种作用域,全局变量和函数的内部变量.在函数内部任何一个地方定义的变量(var scope)其作用域都是整个函数体. 全局变量:指的是window对象下的对象属性. 作用域划分:基于上下文,以函数进行划分的,而不是由块划分的. 强调两点: 1. 在同一作用域中,JavaScript是允许变量的重复定义,并且后一个定义将覆盖前一个定义. 2. 函数内部如果不加关键字var而定义的变量,默认为全局变量. var scope="glob

  • 浅谈JavaScript中的作用域和闭包问题

    JavaScript的作用域以函数为界,不同的函数拥有相对独立的作用域.函数内部可以声明和访问全局变量,也可以声明局部变量(使用var关键字,函数的参数也是局部变量),但函数外部无法访问内部的局部变量: function test() { var a = 0; // 局部变量 b = 1; // 全局变量 } a = ?, b = ? // a为undefined,b为1 同名的局部变量会覆盖全局变量,但本质上它们是两个独立的变量,一方发生变化不会影响另一方: a = 5; // 函数外a的值为

  • 深入浅析JavaScript中的作用域和上下文

    javascript中的作用域(scope)和上下文(context)是这门语言的独到之处,这部分归功于他们带来的灵活性.每个函数有不同的变量上下文和作用域.这些概念是javascript中一些强大的设计模式的后盾.然而这也给开发人员带来很大困惑.下面全面揭示了javascript中的上下文和作用域的不同,以及各种设计模式如何使用他们. 上下文(Context)和作用域(Scope) 首先需要知道的是,上下文和作用域是两个完全不同的概念.多年来,我发现很多开发者会混淆这两个概念(包括我自己),

  • javascript中的作用域和上下文使用简要概述

    javascript中的作用域(scope)和上下文(context)是这门语言的独到之处,这部分归功于他们带来的灵活性.每个函数有不同的变量上下文和作用域.这些概念是javascript中一些强大的设计模式的后盾.然而这也给开发人员带来很大困惑.下面全面揭示了javascript中的上下文和作用域的不同,以及各种设计模式如何使用他们. 上下文 vs 作用域 首先需要澄清的问题是上下文和作用域是不同的概念.多年来我注意到许多开发者经常将这两个术语混淆,错误的将一个描述为另一个.平心而论,这些术语

  • Javascript中的作用域和上下文深入理解

    概述 Javascript中的作用域和上下文的实现是Javascript语言独有的特性,从某种程度上来说,Javascript语言是十分灵活的.Javascript中的函数可以采用各种各样的上下文,作用域也可以被封装和保存.正是由于这些特性,Javascript中也提供了很多很有用的设计模式.然而,作用域和上下文也是Javascript程序员在开发中经常迷惑的地方. 下面会向大家介绍Javascript中作用域和上下文的概念,以及它们的不同. 作用域 VS 上下文 首先要说明的很重要的一点是作用

  • Javascript中的作用域及块级作用域

    一.块级作用域的说明 在学习JavaScript的变量作用域之前,我们应当明确几点: a.JavaScript的变量作用域是基于其特有的作用域链的. b.JavaScript没有块级作用域. c.函数中声明的变量在整个函数中都有定义. javascript的变量作用域,与平时使用的类C语言不同,例如C#中的代码: static void Main(string[] args) { if(true) { int number=10; } Console.WriteLine(number); } 这

  • 深入浅析javascript中的作用域(推荐)

    所谓的作用域,可以简单理解为一个可以读.写的范围(区域),有些js经验的同学可能会说:"js没有块级作用域",js除了全局作用域外,只有函数可以创建作用域.作用域的一个好处就是可以隔离变量. 我们通过一些例子来帮助我们理解js中的作用域. alert(a); var a = 1; 如果对作用域一点不了解的同学可能会说 alert的是1或者报错:但实际上是undefined: 说到这里,我们首先说一下js逐行解析代码之前做的一些准备工作, js在逐行读代码之前,会做一些"预解析

  • JavaScript中的作用域链和闭包

    作用域 全局作用域 局部作用域 作用域链 执行上下文 活动对象 闭包 闭包优化 JavaScript中出现了一个以前没学过的概念--闭包.何为闭包?从表面理解即封闭的包,与作用域有关.所以,说闭包以前先说说作用域. 作用域(scope) 通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突. 全局作用域(Global Scope) 在代码中任何地方都能访问到的对象拥有全局作用域,以下几种情形拥有全局作

  • 深入理解Javascript中的作用域链和闭包

    首先我们回顾下之前一篇关于介绍数组遍历的文章: 请先看上一篇中提到的for循环代码: var array = []; array.length = 10000000;//(一千万) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date();

随机推荐