JavaScript词法作用域—你不知道的JavaScript上卷读书笔记(一)

时间:2023-02-23 12:08:28

前段时间在每天往返的地铁上抽空将 《你不知道的JavaScript(上卷)》读了一遍,这本书很多部分写的很是精妙,对于接触前端时间不太久的人来说,就好像是叩开了JavaScript的另一扇门,很多内容醍醐灌顶!所以决定将这本书分四个部分整理出来,同时也这本书强烈推荐给正在进阶的小伙伴们。这篇博文主要整理第一部分 作用域

词法作用域

理解作用域

首先要介绍下JS参与程序 var a = 2的处理过程的演员表:

  • 引擎

    从头到尾负责整个JavaScript 程序的编译及执行过程。

  • 编译器

    引擎的好朋友之一,负责语法分析及代码生成等脏活累活

  • 作用域

    引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

在处理过程中,引擎会为变量a 进行LHS 查询。另外一个查找的类型叫作RHS当变量出现在赋值操作的左侧时进行LHS 查询,出现在右侧时进行RHS 查询。

console.log(a)  // RHS查询
引擎与作用域的对话
function foo(a) {
console.log( a ); // 2
}
foo( 2 ); 引擎:我说作用域,我需要为foo 进行RHS 引用。你见过它吗?
作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
引擎:哥们太够意思了!好吧,我来执行一下foo。
引擎:作用域,还有个事儿。我需要为a 进行LHS 引用,这个你见过吗?
作用域:这个也见过,编译器最近把它声名为foo 的一个形式参数了,拿去吧。
引擎:大恩不言谢,你总是这么棒。现在我要把2 赋值给a。
引擎:哥们,不好意思又来打扰你。我要为console 进行RHS 引用,你见过它吗?
作用域:咱俩谁跟谁啊,再说我就是干这个。这个我也有,console 是个内置对象。
给你。
引擎:么么哒。我得看看这里面是不是有log(..)。太好了,找到了,是一个函数。
引擎:哥们,能帮我再找一下对a 的RHS 引用吗?虽然我记得它,但想再确认一次。
作用域:放心吧,这个变量没有变动过,拿走,不谢。
引擎:真棒。我来把a 的值,也就是2,传递进log(..)。
作用域嵌套

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

两个常见异常
  1. 如果RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。值得注意的是,ReferenceError 是非常重要的异常类型。

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

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

大部分标准语言编译器的第一个工作阶段叫作词法化,**词法作用域就是定义在词法阶段的作用域.**

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

上面例子中,有三个嵌套的作用域:

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

作用域查找会在找到第一个匹配的标识符时停止

######欺骗词法

有些函数会在运行时修改词法作用域,但是欺骗词法作用域会导致性能下降。

  • 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,并遮蔽了外部(全局)作用域中的同名变量。但是在严格模式下,eval(..) 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。

  • with

     function foo(obj) {
    with (obj) {
    a = 2;
    }
    } var o2 = {
    b: 3
    }; foo( o2 );
    console.log( o2.a ); // undefined
    console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!

当我们将o2 作为作用域时,其中并没有a 标识符,因此进行了正常的LHS 标识符查找。o2 的作用域、foo(..) 的作用域和全局作用域中都没有找到标识符a,因此当a=2 执行时,自动创建了一个全局变量(因为是非严格模式)。

eval(..) 函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。不推荐使用eval(..) 和with 的原因是会被严格模式所影响(限制)。with 被完全禁止,而在保留核心功能的前提下,间接或非安全地使用eval(..) 也被禁止了。

函数作用域与块作用域

函数作用域

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

  1. 函数声明与函数表达式

    区分函数声明和表达式最简单的方法是看function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

    函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。

匿名与具名

	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 );

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

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

块级作用域

for (var i=0; i<10; i++) {
console.log( i );
}
// 为什么要把一个只在for 循环内部使用(至少是应该只在内部使用)的变量i 污染到整个函数作用域中呢?

常用的块级作用域:

  1. with

  2. try/catch

     try {
    undefined(); // 执行一个非法操作来强制制造一个异常
    }
    catch (err) {
    console.log( err ); // 能够正常执行!
    }
    console.log( err ); // ReferenceError: err not found
  3. let/const (这两为ES6最基本的关键字,就不多介绍了,但是很重要!)

提升

先有鸡还是先有蛋

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

上面代码你认为会输出什么?很多开发者会认为是undefined,因为var a 声明在a = 2 之后,他们自然而然地认为变量被重新赋值了,因此会被赋予默认值undefined。但是,真正的输出结果是2。包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

当你看到var a = 2; 时,可能会认为这是一个声明。但JavaScript 实际上会将其看成两个声明:var a; 和a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。

刚才的代码会被处理为:

var a;        //提升
a = 2;
console.log( a );
先有蛋(声明)后有鸡(赋值)

函数优先

函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个“重复”声明的代码中)是函数会首先被提升,然后才是变量。考虑以下代码:

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 );
};

最后提一点:js只有词法作用域,无动态作用域。但是this 机制某种程度上很像动态作用域。他们主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

JavaScript词法作用域—你不知道的JavaScript上卷读书笔记(一)的更多相关文章

  1. 网易JS面试题与Javascript词法作用域说明

    调用对象位于作用域链的前端,局部变量(在函数内部用var声明的变量).函数参数及Arguments对象都在函数内的作用域中--这意味着它们隐藏了作用域链更上层的任何同名的属性. 2010年9月14日, ...

  2. 《JavaScript面向对象的编程指南》--读书笔记

    第一章.引言 1.5 面向对象的程序设计常用概念 对象(名词):是指"事物"在程序设计语言中的表现形式. 这里的事物可以是任何东西,我们可以看到它们具有某些明确特征,能执行某些动作 ...

  3. JS闭包—你不知道的JavaScript上卷读书笔记(二)

    关于闭包,初学者会被绕的晕头转向,在学习的路上也付出了很多精力来理解. 让我们一起来揭开闭包神秘的面纱. 闭包晦涩的定义 看过很多关于闭包的定义,很多讲的云里雾里,晦涩难懂.让不少人以为闭包是多么玄乎 ...

  4. JavaScript 词法作用域不完全指北

    在 JavaScript 作用域不完全指北 中,我们介绍了作用域的概念以及 JavaScript 引擎.编译器和作用域的关系.作用域有两种主要的工作模型:词法作用域和动态作用域.其中最为普遍的也是大多 ...

  5. JavaScript词法作用域与调用对象

    关于 Javascript 的函数作用域.调用对象和闭包之间的关系很微妙,关于它们的文章已经有很多,但不知道为什么很多新手都难以理解.我就尝试用比较通俗的语言来表达我自己的理解吧. 作用域 Scope ...

  6. Professional JavaScript for Web Developers 3rd Edition ---读书笔记

    1. DOMContentLoaded DOM树构建完成时触发该事件 load 页面加载完毕触发 原生js document.addEventListener('DOMContentLoaded', ...

  7. 为JavaScript正名--读你不知道的JavaScript&lpar;持续更新&period;&period;&rpar;

    你不知道的JavaScript上卷 JavaScript和Java的关系就像Carnival和Car的关系一样,八竿子打不着. JavaScript易上手,但由于其本身的特殊性,相比其他语言能真正掌握 ...

  8. 说说循环与闭包——《你不知道的JS》读书笔记(一)

    什么是闭包 <你不知道的JS>里有对闭包的定义:"当函数可以记住并访问所在的词法作用域,即使函数是在当前作用域之外执行,这就产生了闭包." 讲闭包是啥的太多了...就一 ...

  9. JavaScript中的this—你不知道的JavaScript上卷读书笔记(三)

    this是什么? this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件.this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式.当一个函数被调用时,会 ...

随机推荐

  1. HDU 1575

    Tr A Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submis ...

  2. 【转】Linux系统启动过程分析

    [转]Linux系统启动过程分析 转自:http://blog.chinaunix.net/uid-23069658-id-3142047.html 经过对Linux系统有了一定了解和熟悉后,想对其更 ...

  3. 【Android UI设计与开发】7&period;底部菜单栏(四)PopupWindow 实现显示仿腾讯新闻底部弹出菜单

    前一篇文章中有用到 PopupWindow 来实现弹窗的功能.简单介绍以下吧. 官方文档是这样解释的:这就是一个弹出窗口,可以用来显示一个任意视图.出现的弹出窗口是一个浮动容器的当前活动. 1.首先来 ...

  4. 编译inotify报错

    错误如下: configure: error: no acceptable C compiler found in $PATH 这是因为没有找到编译器的原因造成的 解决方法: 安装gcc [root@ ...

  5. Linux性能统计工具

    Linux下的一些I/O统计工具 http://blog.csdn.net/longxibendi/article/details/36004155

  6. CSS居中方法搜集

    转自这里:http://jinlong.github.io/blog/2013/08/13/centering-all-the-directions/ 兼容低版本IE的方法 html使用表格结构 背景 ...

  7. OSI七层模型详解

    OSI 七层模型通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯,因此其最主要的功能就是帮助不同类型的主机实现数据传输 . 完成中继功能的节点通常称为中继系统.在OSI七层模型中,处于 ...

  8. P3200 &lbrack;HNOI2009&rsqb;有趣的数列--洛谷luogu

    ---恢复内容开始--- 题目描述 我们称一个长度为2n的数列是有趣的,当且仅当该数列满足以下三个条件: (1)它是从1到2n共2n个整数的一个排列{ai}: (2)所有的奇数项满足a1<a3& ...

  9. MongoDB数据库备份与还原、单表的导入导出

    -------------------MongoDB备份与恢复------------------- 1.MongoDB数据库备份     1.语法:         mongodump -h dbh ...

  10. 关于cxf生成客户端代码中的JAXBElement&lt&semi;String&gt&semi;

    1.使用自动生成的java文件中的 ObjectFactory构造入参 关于cxf生成客户端代码中的JAXBElement<String>    在使用cxf或者x-fire进行webse ...