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

介绍

JavaScript中有一个被称为作用域(Scope)的特性。虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,我会尽我所能用最简单的方式来解释作用域。理解作用域将使你的代码脱颖而出,减少错误,并帮助您使用它强大的设计模式。

什么是作用域(Scope)?

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。

为什么说作用域是最小访问原则?

那么,为什么要限制变量的可见性呢,为什么你的变量不是在代码的任何地方都可用呢?一个优点是作用域为您的代码提供了一定程度的安全性。计算机安全的一个常见原则是用户应该一次只能访问他们需要的东西。

想象一下计算机管理员。由于他们对公司的系统有很多控制权限,因此向他们授予超级管理员权限就好了。他们都可以完全访问系统,一切工作顺利。但突然发生了一些坏事,你的系统感染了恶意病毒。现在你不知道谁犯的错误?你意识到应该授予普通用户权限,并且只在需要时授予超级访问权限。这将帮助您跟踪更改,并记录谁拥有什么帐户。这被称为最小访问原则。看起来很直观?这个原则也适用于编程语言设计,在大多数编程语言中被称为作用域,包括我们接下来要研究的 JavaScript 。

当你继续在你的编程旅程,您将意识到,您的代码的作用域有助于提高效率,帮助跟踪错误并修复它们。作用域还解决了命名问题,在不同作用域中变量名称可以相同。记住不要将作用域与上下文混淆。它们的特性不同。

JavaScript中的作用域

在JavaScript中有两种类型的作用域:

全局作用域

局部作用域(也叫本地作用域)

定义在函数内部的变量具有局部作用域,而定义在函数外部的变量具有全局范围内。每个函数在被调用时都会创建一个新的作用域。

全局作用域

当您开始在文档中编写JavaScript时,您已经在全局作用域中了。全局作用域贯穿整个javascript文档。如果变量在函数之外定义,则变量处于全局作用域内。

JavaScript代码:

// 默认全局作用域

varname='Hammad';

在全局作用域内的变量可以在任何其他作用域内访问和修改。

JavaScript代码:

varname='Hammad';

console.log(name);// logs 'Hammad'

functionlogName(){

console.log(name);// 'name' 可以在这里和其他任何地方被访问

}

logName();// logs 'Hammad'

局部作用域

函数内定义的变量在局部(本地)作用域中。而且个函数被调用时都具有不同的作用域。这意味着具有相同名称的变量可以在不同的函数中使用。这是因为这些变量被绑定到它们各自具有不同作用域的相应函数,并且在其他函数中不可访问。

JavaScript代码:

// Global Scope

functionsomeFunction(){

// Local Scope #1

functionsomeOtherFunction(){

// Local Scope #2

}

}

// Global Scope

functionanotherFunction(){

// Local Scope #3

}

// Global Scope

块语句

块语句,如if和switch条件语句或for和while循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。

JavaScript代码:

if(true){

// 'if' 条件语句块不会创建一个新的作用域

varname='Hammad';// name 依然在全局作用域中

}

console.log(name);// logs 'Hammad'

ECMAScript 6 引入了let和const关键字。可以使用这些关键字来代替var关键字。

JavaScript代码:

varname='Hammad';

let likes='Coding';

constskills='Javascript and PHP';

与var关键字相反,let和const关键字支持在局部(本地)作用域的块语句中声明。

JavaScript代码:

if(true){

// 'if' 条件语句块不会创建一个新的作用域

// name 在全局作用域中,因为通过 'var' 关键字定义

varname='Hammad';

// likes 在局部(本地)作用域中,因为通过 'let' 关键字定义

let likes='Coding';

// skills 在局部(本地)作用域中,因为通过 'const' 关键字定义

constskills='JavaScript and PHP';

}

console.log(name);// logs 'Hammad'

console.log(likes);// Uncaught ReferenceError: likes is not defined

console.log(skills);// Uncaught ReferenceError: skills is not defined

只要您的应用程序生活,全球作用域就会生存。 只要您的函数被调用并执行,局部(本地)作用域就会存在。

上下文

许多开发人员经常混淆 作用域(scope) 和 上下文(context),很多误解为它们是相同的概念。但事实并非如此。作用域(scope)我们上面已经讨论过了,而上下文(context)是用来指定代码某些特定部分中this的值。作用域(scope) 是指变量的可访问性,上下文(context)是指this在同一作用域内的值。我们也可以使用函数方法来改变上下文,将在稍后讨论。 在全局作用域(scope)中上下文中始终是Window对象。(愚人码头注:取决于JavaScript 的宿主换环境,在浏览器中在全局作用域(scope)中上下文中始终是Window对象。在Node.js中在全局作用域(scope)中上下文中始终是Global对象)

JavaScript代码:

// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}

console.log(this);

functionlogFunction(){

console.log(this);

}

// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}

// 因为 logFunction() 不是一个对象的属性

logFunction();

如果作用域在对象的方法中,则上下文将是该方法所属的对象。

ES6代码:

classUser{

logName(){

console.log(this);

}

}

(newUser).logName();// logs User {}

(new User).logName() 是一种将对象存储在变量中然后调用logName函数的简单方法。在这里,您不需要创建一个新的变量。

您会注意到,如果您使用new关键字调用函数,则上下文的值会有所不同。然后将上下文设置为被调用函数的实例。考虑上面的示例,通过new关键字调用的函数。

JavaScript代码:

functionlogFunction(){

console.log(this);

}

newlogFunction();// logs logFunction {}

当在严格模式(Strict Mode)中调用函数时,上下文将默认为undefined。

执行期上下文(Execution Context)

愚人码头注:这部分解释建议先查看这篇文章,更加通俗易懂,http://www.css88.com/archives/7262

上面我们了解了作用域和上下文,为了消除混乱,特别需要注意的是,执行期上下文中的上下文这个词语是指作用域而不是上下文。这是一个奇怪的命名约定,但由于JavaScipt规范,我们必须链接他们这间的联系。

JavaScript是一种单线程语言,因此它一次只能执行一个任务。其余的任务在执行期上下文中排队。正如我刚才所说,当 JavaScript 解释器开始执行代码时,上下文(作用域)默认设置为全局。这个全局上下文附加到执行期上下文中,实际上是启动执行期上下文的第一个上下文。

之后,每个函数调用(启用)将其上下文附加到执行期上下文中。当另一个函数在该函数或其他地方被调用时,会发生同样的事情。

每个函数都会创建自己的执行期上下文。

一旦浏览器完成了该上下文中的代码,那么该上下文将从执行期上下文中销毁,并且执行期上下文中的当前上下文的状态将被传送到父级上下文中。 浏览器总是执行堆栈顶部的执行期上下文(这实际上是代码中最深层次的作用域)。

无论有多少个函数上下文,但是全局上下文只有一个。

执行期上下文有创建和代码执行的两个阶段。

创建阶段

第一阶段是创建阶段,当一个函数被调用但是其代码还没有被执行的时。 在创建阶段主要做的三件事情是:

创建变量(激活)对象

创建作用域链

设置上下文(context)的值( `this` )

变量对象

变量对象,也称为激活对象,包含在执行期上下文中定义的所有变量,函数和其他声明。当调用函数时,解析器扫描它所有的资源,包括函数参数,变量和其他声明。包装成一个单一的对象,即变量对象。

JavaScript代码:

'variableObject':{

// 包含函数参数,内部变量和函数声明

}

作用域链

在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量或其他任何资源为止。作用域链可以简单地定义为包含其自身执行上下文的变量对象的对象,以及其父级对象的所有其他执行期上下文,一个具有很多其他对象的对象。

JavaScript代码:

'scopeChain':{

// 包含自己的变量对象和父级执行上下文的其他变量对象

}

执行期上下文对象

执行期上下文可以表示为一个抽象对象,如下所示:

JavaScript代码:

executionContextObject={

'scopeChain':{},// 包含自己的变量对象和父级执行上下文的其他变量对象

'variableObject':{},// 包含函数参数,内部变量和函数声明

'this':valueOfThis

}

代码执行阶段

在执行期上下文的第二阶段,即代码执行阶段,分配其他值并最终执行代码。

词法作用域

词法作用域意味着在一组嵌套的函数中,内部函数可以访问其父级作用域中的变量和其他资源。这意味着子函数在词法作用域上绑定到他们父级的执行期上下文。词法作用域有时也被称为静态作用域。

JavaScript代码:

functiongrandfather(){

varname='Hammad';

// likes 在这里不可以被访问

functionparent(){

// name 在这里可以被访问

// likes 在这里不可以被访问

functionchild(){

// 作用域链最深层

// name 在这里也可以被访问

varlikes='Coding';

}

}

}

你会注意到词法作用域向内传递的,意味着name可以通过它的子级期执行期上下文访问。但是,但是它不能向其父对象反向传递,意味着变量likes不能被其父对象访问。这也告诉我们,在不同执行上下文中具有相同名称的变量从执行堆栈的顶部到底部获得优先级。在最内层函数(执行堆栈的最上层上下文)中,具有类似于另一变量的名称的变量将具有较高优先级。

闭包( Closures)

愚人码头注:这部分解释建议先查看这篇文章,更加通俗易懂,http://www.css88.com/archives/7262

闭包的概念与我们在上面讲的词法作用域密切相关。 当内部函数尝试访问其外部函数的作用域链,即在直接词法作用域之外的变量时,会创建一个闭包。 闭包包含自己的作用域链,父级的作用域链和全局作用域。

闭包不仅可以访问其外部函数中定义的变量,还可以访问外部函数的参数。

即使函数返回后,闭包也可以访问其外部函数的变量。这允许返回的函数保持对外部函数所有资源的访问。

当从函数返回内部函数时,当您尝试调用外部函数时,不会调用返回的函数。您必须首先将外部函数的调用保存在单独的变量中,然后将该变量调用为函数。考虑这个例子:

JavaScript代码:

functiongreet(){

name='Hammad';

returnfunction(){

console.log('Hi '+name);

}

}

greet();// 什么都没发生,没有错误

// 从 greet() 中返回的函数保存到 greetLetter 变量中

greetLetter=greet();

// 调用  greetLetter 相当于调用从 greet() 函数中返回的函数

greetLetter();// logs 'Hi Hammad'

这里要注意的是,greetLetter函数即使在返回后也可以访问greet函数的name变量。 有一种方法不需要分配一个变量来访问greet函数返回的函数,即通过使用两次括号(),即()()来调用,就是这样:

JavaScript代码:

functiongreet(){

name='Hammad';

returnfunction(){

console.log('Hi '+name);

}

}

greet()();// logs 'Hi Hammad'

公共作用域和私有作用域

在许多其他编程语言中,您可以使用公共,私有和受保护的作用域来设置类的属性和方法的可见性。考虑使用PHP语言的这个例子:

PHP代码:

// Public Scope

public$property;

publicfunctionmethod(){

// ...

}

// Private Sccpe

private$property;

privatefunctionmethod(){

// ...

}

// Protected Scope

protected$property;

protectedfunctionmethod(){

// ...

}

来自公共(全局)作用域的封装函数使他们免受脆弱的攻击。但是在JavaScript中,没有公共或私有作用域。幸好,我们可以使用闭包来模拟此功能。为了保持一切与全局分离,我们必须首先将我们的函数封装在如下所示的函数中:

JavaScript代码:

(function(){

// 私有作用域 private scope

})();

函数末尾的括号会告知解析器在没有调用的情况下一旦读取完成就立即执行它。(愚人码头注:这其实叫立即执行函数表达式)我们可以在其中添加函数和变量,它们将不能在外部访问。但是,如果我们想在外部访问它们,也就是说我们希望其中一些公开的,另一些是私有的?我们可以使用一种称为 模块模式 的闭包类型,它允许我们使用对象中公共和私有的作用域来对我们的函数进行调整。

模块模式

模块模式类似这样:

JavaScript代码:

varModule=(function(){

functionprivateMethod(){

// do something

}

return{

publicMethod:function(){

// can call privateMethod();

}

};

})();

Module中的return语句包含了我们公开的函数。私有函数只是那些没有返回的函数。没有返回的函数不可以在Module命名空间之外访问。但是公开函数可以访问私有函数,这使它们对于助手函数,AJAX调用和其他事情很方便。

JavaScript代码:

Module.publicMethod();// 可以正常工作

Module.privateMethod();// Uncaught ReferenceError: privateMethod is not defined

私有函数一个惯例是用下划线开始,并返回一个包含我们公共函数的匿名对象。这使得它们很容易在长对象中管理。它看起来是这样子的:

JavaScript代码:

varModule=(function(){

function_privateMethod(){

// do something

}

functionpublicMethod(){

// do something

}

return{

publicMethod:publicMethod,

}

})();

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

另一种类型的闭包是立即执行函数表达式(IIFE)。这是一个在window上下文中调用的自动调用的匿名函数,这意味着this的值为window。暴露一个单一的全局接口来进行交互。他是这样的:

JavaScript代码:

(function(window){

// do anything

})(this);

使用 .call(), .apply() 和 .bind() 改变上下文

.call()和.apply()函数用于在调用函数时改变上下文。这给了你令人难以置信的编程能力(和一些终极权限来驾驭代码)。

要使用call或apply函数,您只需要在函数上调用它,而不是使用一对括号调用函数,并将新的上下文作为第一个参数传递。

函数自己的参数可以在上下文之后传递。(愚人码头注:call或apply用另一个对象来调用一个方法,将一个函数上下文从初始的上下文改变为指定的新对象。简单的说就是改变函数执行的上下文。)

JavaScript代码:

functionhello(){

// do something...

}

hello();// 通常的调用方式

hello.call(context);// 在这里你可以传递上下文(this 值)作为第一个参数

hello.apply(context);// 在这里你可以传递上下文(this 值)作为第一个参数

.call()和.apply()之间的区别在于,在.call()中,其余参数作为以逗号分隔的列表,而.apply()则允许您在数组中传递参数。

JavaScript代码:

functionintroduce(name,interest){

console.log('Hi! I\'m '+name+' and I like '+interest+'.');

console.log('The value of this is '+this+'.')

}

introduce('Hammad','Coding');// 通常的调用方式

introduce.call(window,'Batman','to save Gotham');// 在上下文之后逐个传递参数

introduce.apply('Hi',['Bruce Wayne','businesses']);// 在上下文之后传递数组中的参数

// 输出:

// Hi! I'm Hammad and I like Coding.

// The value of this is [object Window].

// Hi! I'm Batman and I like to save Gotham.

// The value of this is [object Window].

// Hi! I'm Bruce Wayne and I like businesses.

// The value of this is Hi.

.call()的性能要比.apply()稍快。

以下示例将文档中的项目列表逐个记录到控制台。

HTML 代码:

Things to learn

Things to Learn to Rule the World

    Learn PHP

    Learn Laravel

    Learn JavaScript

    Learn VueJS

    Learn CLI

    Learn Git

    Learn Astral Projection

    // 在listItems中保存页面上所有列表项的NodeList

    varlistItems=document.querySelectorAll('ul li');

    // 循环遍历listItems NodeList中的每个节点,并记录其内容

    for(vari=0;i

    (function(){

    console.log(this.innerHTML);

    }).call(listItems[i]);

    }

    // Output logs:

    // Learn PHP

    // Learn Laravel

    // Learn JavaScript

    // Learn VueJS

    // Learn CLI

    // Learn Git

    // Learn Astral Projection

    HTML仅包含无序的项目列表。然后 JavaScript 从DOM中选择所有这些项目。列表循环,直到列表中的项目结束。在循环中,我们将列表项的内容记录到控制台。

    该日志语句包裹在一个函数中,该call函数包含在调用函数中的括号中。将相应的列表项传递给调用函数,以便控制台语句中的this关键字记录正确对象的 innerHTML 。

    对象可以有方法,同样的函数对象也可以有方法。 事实上,JavaScript函数附带了四种内置方法:

    Function.prototype.apply()

    Function.prototype.bind() ( ECMAScript 5 (ES5) 中引进)

    Function.prototype.call()

    Function.prototype.toString()

    Function.prototype.toString() 返回函数源代码的字符串表示形式。

    到目前为止,我们讨论过.call(),.apply()和toString()。与.call()和.apply()不同,.bind()本身不调用该函数,它只能用于在调用函数之前绑定上下文和其他参数的值。在上面的一个例子中使用.bind():

    JavaScript代码:

    (functionintroduce(name,interest){

    console.log('Hi! I\'m '+name+' and I like '+interest+'.');

    console.log('The value of this is '+this+'.')

    }).bind(window,'Hammad','Cosmology')();

    // logs:

    // Hi! I'm Hammad and I like Cosmology.

    // The value of this is [object Window].

    .bind()就像.call()函数一样,它允许你传递其余的参数,用逗号分隔,而不是像apply,在数组中传递参数。

    结论

    这些概念是 JavaScript 的根本,对于了解高级语法很重要。我希望你能更好地了解JavaScript作用域和他相关的事情。如果没用弄明白这些问题,欢迎在下面的评论中提问。

    学习更多

    原文地址:https://scotch.io/tutorials/understanding-scope-in-javascript

    最后编辑于
    ©著作权归作者所有,转载或内容合作请联系作者
    • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
      沈念sama阅读 159,015评论 4 362
    • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
      沈念sama阅读 67,262评论 1 292
    • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
      开封第一讲书人阅读 108,727评论 0 243
    • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
      开封第一讲书人阅读 43,986评论 0 205
    • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
      茶点故事阅读 52,363评论 3 287
    • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
      开封第一讲书人阅读 40,610评论 1 219
    • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
      沈念sama阅读 31,871评论 2 312
    • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
      开封第一讲书人阅读 30,582评论 0 198
    • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
      沈念sama阅读 34,297评论 1 242
    • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
      茶点故事阅读 30,551评论 2 246
    • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
      茶点故事阅读 32,053评论 1 260
    • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
      沈念sama阅读 28,385评论 2 253
    • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
      茶点故事阅读 33,035评论 3 236
    • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
      开封第一讲书人阅读 26,079评论 0 8
    • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
      开封第一讲书人阅读 26,841评论 0 195
    • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
      沈念sama阅读 35,648评论 2 274
    • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
      茶点故事阅读 35,550评论 2 270

    推荐阅读更多精彩内容