JavaScript夯实基础系列(一):词法作用域

时间:2021-09-02 08:18:23

  作用域是一组规则,规定了引擎如何通过标识符名称来查询一个变量。作用域模型有两种:词法作用域动态作用域。词法作用域是在编写时就已经确定的:通过阅读包含变量定义的数行源码就能知道变量的作用域。JavaScript采用的是词法作用域,也称为执行环境。动态作用域不是在代码编写时静态决定的,而是在执行过程中被确定。JavaScript实际上没有动态作用域,但是this的用法有些像动态作用域。静态作用域关心函数在何处被声明,而动态作用域关心函数在何处被调用。

一、作用域链

  在ES6之前,一般认为JavaScript只有全局作用域以及基于函数的作用域,没有块级作用域(有两种特殊情况:with以及try/catch中的catch块,with在严格模式下不能使用,基本已废弃,不推荐使用。catch的块级作用域性质不常用,可用来ES6块级作用域的Polyfill)。ES规范强制规定全局变量是全局对象的属性,但是对于局部变量没有类似的规定。局部变量基本是函数内的变量(没有使用varlet或者const声明的变量为全局变量)以及函数参数。一般认为,局部变量是一个跟所在函数关联的变量对象的属性。这个变量对象在ES3中叫调用对象,在ES5中叫声明上下文对象,该对象对我们是透明的,不可见。全局对象我们可以通过this关键字来引用。

  当程序执行到函数中时,会产生一个跟该函数相关的作用域链,并把作用域链赋值给函数的一个特殊的内部属性(即[[Scope]]),作用域链是一个对象列表或者链表。拿作用域链的链表实现来说,链表的尾结点是全局对象,嵌套函数的每一层函数对应链表上的一个节点,该节点包含两个指针,分别指向函数对应的变量对象和包含函数对应的节点。注意:每一个执行中得函数都有一个作用域链,包含函数与被包含函数并不是在同一个作用域链上。可以想象,当程序从包含函数流进被包含函数时,被包含函数的作用域链生成过程是:先复制包含函数的作用域链,然后生成一个节点,该节点包含一个指向自身函数对应的变量对象,最后以前插法的方式将该节点插入到新生成链表的头部,所有链表的尾部节点都是指向全局对象。

  JavaScript在查找一个变量时,即变量解析,会从对应作用域链的头部开始查,如果在头部指针指向的变量对象中找到该变量,则停止查询,采用找到的该变量的值。如果没有找到,则沿着链表逐级查询,直到查询到链表尾部节点对应的变量对象,即全局对象,如果最终没有查询到变量,则会报ReferenceError的错误。

  例如:下面代码的作用域链如下图所示。全局变量的作用域链只包含指向全局对象的指针;函数a的作用域链有两个节点,分别指向函数a对应的变量对象和全局对象;函数b的作用域链有三个节点,分别对应指向b对应的变量对象、指向a对应的变量对象和全局对象。函数a不能访问函数b中的变量test3,因为在函数a的作用域链中并不包括函数b对应的变量对象。如果在函数a中使用test3,解析变量test3时,首先从a的作用域链头部开始,查询a对应的变量对象,然后查询全局对象,都没有找到,程序会提示引用错误。

var test1 = 'global' // 全局变量
a() function a () {
var test2 = 'a' // 函数a的局部变量
console.log(test2)
b() function b () {
var test3 = 'b' // 函数b的局部变量
console.log(test2 + test3)
}
}

JavaScript夯实基础系列(一):词法作用域

  有一种特殊情况需要注意,函数的变量对象上存在两个特殊的变量:thisarguments,函数搜索这两个变量时只会在对应的变量对象上搜索,不会沿着作用域链搜索外层环境的变量对象。

二、延长作用域链

  JavaScript中有两种方式可以延长作用域链,ES3之前的with以及ES3添加的try/catch。程序执行到这两个语句时,会在作用域链的最前端添加一个指向新变量对象的节点,该变量对象会在程序执行完毕后被移除。

1、with

  当我们多次使用一个对象的属性时,每次都需要在需要调用的属性前加上对象,写法相对繁琐,使用with可以减少所写代码量。

function getUrl () {
var obj = {}
obj.a = location.hash
obj.b = location.href
return obj
} // 使用 with 改写
function useWith () {
var obj = {}
with(location) {
obj.a = hash
obj.b = href
}
return obj
}

  with会将指定的对象添加到作用域链中。值得注意的事,在查询变量的时候才会用到作用域链,创建新变量的时候不使用。因此在使用with时,有些时候会遇到一些奇怪的情况。如下代码所示:

function test(obj) {
with (obj) {
a = 2;
}
} var obj1 = { a: 3 };
var obj2 = { b: 3 }; test( obj1 );
console.log( obj1.a ); // 2 test( obj2 );
console.log( obj2.a ); // undefined
console.log( a ); // 2 ---全局作用域被泄漏了!

  在函数test()执行时,with语句延长了函数test的作用域链,对象obj将被添加到作用域链中。分别将obj1obj2作为参数传递给函数时,JavaScript引擎会沿着作用域链查询变量。当参数是obj1时,引擎查找到其拥有该变量,因此给obj1a属性复制为2。当参数是obj2时,引擎没有在obj2中找到该变量,接着在函数test的变量对象上查询,仍然没有找到该变量,最后在全局对象上查询。在都没有找到情况下,a=2语句分两步执行,首先创建一个新的全局变量a,然后从作用域链重新执行一遍查找,最终在全局对象上找到变量a,执行赋值操作,导致全局作用域被泄漏。

  with语句在严格模式下不能使用,在非严格模式下也不提倡使用,该语句被废弃的原因主要是性能问题。JavaScript引擎在编译阶段会做很多性能优化的工作,优化的方法之一就是在词法分析的时候静态的分析代码,提前决定变量和函数声明在什么位置,执行的时候能够更快速。with语句可以添加指定对象到作用域链,JavaScript引擎没办法提前分析你会往这个作用域链的顶端放什么样的变量对象,因此在词法分析时通过分析代码来决定变量和函数声明位置的结果将被全部推翻,使得优化变的毫无意义,没有经过优化的代码肯定比优化过的代码慢。

2、try/catch

  try/catch/finally语句是JavaScript的异常处理机制。try从句定义了需要处理的异常所在的代码块,当try块代码出现异常时,就会调用catch块中的的代码。不论try块中的代码是否出现异常,finally块内的代码必定执行。

try {
undefined(); // 用非法的操作强制产生一个异常!
}
catch (e) {
e = 1
console.log(e); // 1
} console.log(e); // ReferenceError: `err` not found

  catch块会创建一个新的变量对象,将变量对象添加到作用域链顶端。换而言之,catch块中定义的变量拥有块级作用域。上面代码显示的变量e只存在于catch块,在外部引用会报错。catch块的块级作用域性质可以shim ES6中新添加的letconst等块级作用域。在IE8以及之前的版本中,catch块捕获的变量对象会添加到所在函数的变量对象上,如果不被函数包裹,则会添加到全局对象上。

三、动态声明变量

  eval()是全局对象的一个函数属性,eval()函数会将传入的字符串当做JavaScript代码执行,这种动态生成代码的方式有能力修改词法作用域。如下代码:

function test(str) {
eval( str );
console.log( a ); // 2
} test( "var a = 2" );

  在严格模式下,传入eval()的代码不能在调用程序的上下文中声明变量和定义函数,变量和函数的定义是在eval()创建的新作用域中,这个作用域在eval()返回时就弃用了。如下代码:

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

  另外,setTimeout()和setInterval()的第一个参数以及Function()的第最后一个参数都是接收一个字符串来动态生成代码。

  动态生成代码会使JavaScript引擎在编译阶段几乎不能通过做词法分析来进行优化,因此使用动态生成代码会导致很差的性能,一般不提倡使用。

四、块级作用域

  ES6新增了块级作用域,let和const可以用来声明块级变量。

1、let

  let关键字将变量声明附着在所在的块作用域,使得变量只在所在的块有定义。如下代码所示:

var test = true
if(test){
let a = 1
var b = 2
}
console.log(a) // ReferenceError: a is not defined
console.log(b) // 2

  与var关键字不同,let不存在变量提升,let关键字声明的变量一定要在声明之后再使用。如下代码中可以看到var声明的变量在声明前就可以使用,let声明的则不然。

console.log(a) // undefined
var a = 1 console.log(b) // ReferenceError: b is not defined
let b = 2

  let关键字会产生一种叫做暂时性死区的情况。简而言之,在代码块中,使用let关键字声明变量之前,该变量都是不可用的。如下代码所示:

var a = 1
{
console.log(a) // ReferenceError: a is not defined
let a =2
}

  let关键字不允许在相同作用域内重复声明变量。如下代码所示:

let a = 1
let a = 1 // Uncaught SyntaxError: Identifier 'a' has already been declared

  let在循环中会与var有很大的区别,形如for (let x...)的循环在每次迭代时都为x创建新的绑定,改善了循环内变量过度共享的情况。如下代码所示:

var a = [];
var b = []; for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
for (let j = 0; j < 10; j++) {
b[j] = function () {
console.log(j);
};
} a[6](); // 10
b[6](); // 6

  另外,let还有一个很重要的特性。在JavaScript中顶层变量的属性跟全局变量挂钩。顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES6规定:let关键字、const关键字、class关键字声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

2、const

  const关键字拥有上述let关键字的全部特性。此外,const关键字声明变量时,必须赋值。如下代码则会报错:

const a
console.log(a) // Uncaught SyntaxError: Missing initializer in const declaration

  const关键字声明的变量一旦赋值后不能改变,实际是指该变量指向的内存地址不变。简单数据类型,值就保存在指向的内存地址中,而对于对象类型,该变量指向的内存地址中存放的是一个指针,该指针指向存储在堆内存中的对象。const关键字保证了变量指向的内存地址不变,但是不能保证堆内存中对象的数据结构不发生变化。如下代码所示:

const a = 0
a = 1
console.log(a) // Uncaught TypeError: Assignment to constant variable. const b = {}
b.val = 1
console.log(b) // {val:1}

五、总结

  JavaScript中变量分为局部变量和全局变量,局部变量是在函数使用关键字声明的变量,全局变量是指在全局环境下声明或者在函数中没有通过关键字声明的变量。局部变量是所在函数对应的变量对象上的属性,全局变量是全局变量对象的属性,ES规范规定全局变量对象为全局对象。每个执行中的函数都有一个对应的作用域链,作用域链最前端是该函数对应的变量对象,其次是嵌套函数的变量对象,最后是全局对象。JavaScript引擎查询变量时,会沿着作用域链查询,从所处函数的变量对象开始,直至全局对象。在查询过程中,找到要该的变量后查询就会停止,如果直到全局对象都没查询到该变量,则该变量不存在。

  通过with语句和try/catch语句中的catch可以延长作用域链,这两条语句都是在作用域链最前端添加一个新的变量对象。eval()函数在非严格模式下可以动态声明变量来修改作用域,类似的还有setTimeout()、setInterval()以及Function()函数。延长作用域链以及动态声明变量来修改作用域链都会导致性能的降低,词法作用域规则本质上是静态的,JavaScript引擎在编译代码时能通过确定变量在什么位置来进行优化,一旦动态修改作用域链,优化工作很难进行。

  ES6新增letconst来声明块级作用域变量。与var相比,letconst不能重复定义,不存在变量提升,存在暂时性死区。在for循环的时候,var关键字声明的计数变量会被共享,let则不会。const关键字声明变量的时候必须赋值,如果值为基本类型则值不能改变,引用类型则引用类型的地址不能改变。

如需转载,烦请注明出处:https://www.cnblogs.com/lidengfeng/p/9117408.html

JavaScript夯实基础系列(一):词法作用域的更多相关文章

  1. JavaScript夯实基础系列(三):this

      在JavaScript中,函数的每次调用都会拥有一个执行上下文,通过this关键字指向该上下文.函数中的代码在函数定义时不会执行,只有在函数被调用时才执行.函数调用的方式有四种:作为函数调用.作为 ...

  2. JavaScript夯实基础系列(二):闭包

      在JavaScript中函数是一等公民.所谓一等公民是指函数跟其他对象一样,很普通,可以进行把函数存在数组中.作为参数传递.赋值给变量等操作.当函数作为另一个函数的返回值在外部调用时,跟该函数在函 ...

  3. JavaScript夯实基础系列(四):原型

      在JavaScript中有六种数据类型:number.string.boolean.null.undefined以及对象,ES6加入了一种新的数据类型symbol.其中对象称为引用类型,其他数据类 ...

  4. JavaScript夯实基础系列(五):类

      JavaScript中没有类,是通过使用构造函数和原型模式的组合来实现类似其它面向对象编程语言中"类"的功能.ES6引入的关键字class,形式上向其它面向对象编程语言靠拢,其 ...

  5. 夯实基础系列四:Linux 知识总结

    前言 前三节内容传送门: 夯实基础系列一:Java 基础总结 夯实基础系列二:网络知识总结 夯实基础系列三:数据库知识总结 现在很多公司项目部署都使用的是 Linux 服务器,互联网公司更是如此.对于 ...

  6. js基础系列之【作用域】

    声明:形成本文的出发点仅仅是个人总结记录,避免遗忘,并非详实的教程:文中引用了经过个人加工的其它作者的内容,并非原创.学海无涯 什么是作用域? 作用域就是一套规则,用于确定在何处以及如何查找变量(标识 ...

  7. Javascript - 预编译与函数词法作用域

    预编译与函数词法作用域(Precompiled & Scoped) 预编译 Javascript脚本的宿主在执行代码之前对脚本做了预编译处理,比如浏览器对Js进行了预编译,编译器会扫描所有的声 ...

  8. 夯实基础系列一:Java 基础总结

    前言 大学期间接触 Java 的时间也不短了,不论学习还是实习,都让我发觉基础的重要性.互联网发展太快了,各种框架各种技术更新迭代的速度非常快,可能你刚好掌握了一门技术的应用,它却已经走在淘汰的边缘了 ...

  9. 【PHP夯实基础系列】PHP日期,文件系统等知识点

    1. PHP时间 1)strtotime() //日期转成时间戳 2) date()//时间戳变成日期 <?php date_default_timezone_set("PRC&quo ...

随机推荐

  1. Highcharts的基本属性和方法详解

    Highcharts 是一个用纯JavaScript编写的一个图表库, 能够很简单便捷的在web网站或是web应用程序添加有交互性的图表,并且免费提供给个人学习.个人网站和非商业用途使用. 目前Hig ...

  2. &lpar;转&rpar;TeamCity配置笔记

    1.编译sln 2.发布网站 3.重复代码检测 4.代码分析 5.单元测试&覆盖率测试 查看代码覆盖率 7.代码签入时自动触发编译 8.通知 1.在teamcity安装目录中找到TrayNot ...

  3. IOS中使用像素位图&lpar;CGImageRef&rpar;对图片进行处理

    IOS中对图片进行重绘处理的方法总结 一.CGImageRef是什么 CGImageRef是定义在QuartzCore框架中的一个结构体指针,用C语言编写.在CGImage.h文件中,我们可以看到下面 ...

  4. 性能测试分享:MYSQL死锁

    poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨询qq:908821478,咨询电话010-845052 ...

  5. JavaScript学习笔记(十六)——面向对象编程

    在学习廖雪峰前辈的JavaScript教程中,遇到了一些需要注意的点,因此作为学习笔记列出来,提醒自己注意! 如果大家有需要,欢迎访问前辈的博客https://www.liaoxuefeng.com/ ...

  6. xpadder教程:自定义设置游戏手柄的图片

    关于xpadder设置按键的教程,网上已经很多,我就不凑这个热闹了.这里介绍的是如何自定义设置手柄的图片,就是按钮的背景图,如下图所示: 步骤: 1)准备一张背景图 注意:格式必须是24位色的BMP位 ...

  7. 关于Linux虚拟化技术KVM的科普 科普三&lpar;From OenHan&rpar;

    http://oenhan.com/archives,包括<KVM源代码分析1:基本工作原理>.<KVM源代码分析2:虚拟机的创建与运行>.<KVM源代码分析3:CPU虚 ...

  8. 【github&amp&semi;&amp&semi;git】6、SmartGit(试用期30后),个人继续使用的方法。

    在我们做项目的过程中,我们会用到SmartGit这个软件来将本地的MAVEN项目push到国内的码云(https://git.oschina.net)或者是国外的github网站进行项目的管理,这个时 ...

  9. Daily Scrum- 12&sol;31

    Meeting Minutes 更新了统计单词背诵精度的统计数字计算方法: 确定了词反转的效果的动画: Burndown     Progress   part 组员 今日工作 Time (h) 明日 ...

  10. Wamp下安装Memcached

    最近的项目要用到memcache,所以要在本地安装,安装过程中很遇到一些问题,在这里记录下. 我使用的是wamp集成环境,各个版本如下: WampServer Version 2.2 Apache V ...