JS基础学习——作用域
JS基础学习——作用域
什么是作用域
变量的作用域就是变量能被访问到的代码范围,比如在下面的这个js代码中,变量a的作用域就是函数foo,因此在全局作用域内的console.log(a)语句不能访问到变量a,报ReferenceError错误。
function foo()
{
var a =3;
console.log(a);
}
foo();/*3*/
console.log(a);/*ReferenceError: a is not defined*/
作用域可以分为词法作用域和动态作用域两种类型。词法作用域也叫静态作用域,变量的作用域是在词法分析阶段确定的,由变量写在代码上的位置决定,与调用其的上下文无关。动态作用域中变量的作用域是由调用代码的堆栈决定的, 它在代码运行时确定。
比如下面这个例子,如果是词法作用域,bar1()和bar2()都会返回2,因为foo函数中变量a的作用域链为foo函数>全局;但如果是动态作用域的话,bar1()会返回3,bar2()会返回4,即foo函数中的a变量的值与执行环境上下文有关,由调用堆栈动态决定。
var a = 2;
function foo()
{
return a;
}
function bar1()
{
var a = 3;
var b1 = foo();
return b1;
}
function bar2()
{
var a = 4;
var b2 = foo();
return b2;
}
bar1();/*return 2 or 3?*/
bar2();/*return 2 or 4?*/
JS的作用域
JS的作用域属于词法作用域,JS的作用域按作用域范围区分,可以分为全局作用域、函数作用域,且默认不包含块作用域,比如对于下面这段JS代码,因为不存在块作用域,if代码块内部申明的变量a是可以被语句console.log(a)访问到。但在某些特殊情况下,变量可以有块作用域,比如可以通过let、const、catch等关键词设置。
/*-----------code 1----------*/
if(true){
var a = 10;
}
console.log(a);/*10*/
全局作用域
不在函数内部申明的变量或是没有申明就直接使用的变量(非严格模式下),都会成为全局变量,拥有全局作用域,即网页上所有的语句都能访问到它。全局变量会自动成为浏览器全局对象window的属性,所以可以以“window.变量”的形式来引用变量,全局变量在页面关闭后才会消亡。
函数作用域
在函数内部用var声明的变量拥有函数作用域,变量只能被也在该函数内部的语句访问,且它在函数运行完之后立刻消亡。
函数作用域的作用(个人理解)
- 保证代码正确执行,若函数内部的变量能被外部语句访问到,变量的值容易被修改,导致运行结果出错;
- 允许在不同函数内部的函数变量重名,方便代码编写;
- 减少内存空间的占用;
- 隐藏函数内部实现,保证函数私有;
延伸阅读:全局变量和局部变量在内存中的区别
块作用域
有时候我们希望函数内部的部分代码块也能拥有自己的作用域,一方面可以提高代码的可读性更高,同时利用块作用域可以实现最小化变量的作用域,从而更加灵活的控制变量内存的占有和释放。生成块作用的方式如下。
立即执行函数表达IIFE(Immediately Invoking Function Expressions)
立即执行函数表达IIFE形如(function (){ .. })(),第一个括号包裹函数表达,第二个括号表示立即执行函数,第二个括号里可以传递函数所需的参数。
我们知道JS中函数定义的方式之一是函数表达式定义,包括匿名函数表达式定义和内联函数表达式定义,那么IIFE也有两种写法,匿名函数表达如下面code 2所示,内联函数表达如code 3所示,推荐使用第二种,它的好处是可以方便实现函数调用自己,同时代码的可读性比较高。
/*-----------code 2----------*/
var a = 2;
(function (){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2 /*-----------code 3----------*/
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
with关键词
with关键词申明的块的最初目的是为了减少对同一对象的重复引用,方便代码的编写,但是with关键词还有一个特性就是with的包含块是一个独立的作用域,但是这个关键词已经被弃用了。
/*-----------code 4----------*/
var obj = {
a: 1,
b: 2,
c: 3
}; // more "tedious" to repeat "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4; // "easier" short-hand
with (obj) {
a = 3;
b = 4;
c = 5;
}
catch关键词
从ES3开始定义catch的包含块是一个独立的块作用域,所以在code 4中,catch外面的
console.log( a );
是访问不到变量a的。/*-----------code 4----------*/
try{
throw 2;
}catch(a){
console.log( a ); // 2
}
console.log( a ); //ReferenceError: a is not defined
let关键词
ES6引入了let关键字用来为变量创建块作用域,它是代替var关键词的的一种新的变量申明方式。let关键词申明的变量的作用域为包含它的最小{...}内部,如code 5所示。
/*-----------code 5----------*/
{
let i = 1;
};
console.log(i);//ReferenceError: i is not defined
利用let关键字为变量指定现有块为作用域是为变量添加块作用域的隐式写法,这种写法容易混淆变量的作用域,在移动复制代码时容易发生错误,因此建议使用显式写法为变量增加块作用域,即用新的{}给出变量作用域,如code 6所示,这样写更容易进行代码的重构,保证代码语义正确。
/*-----------code 6----------*/
var foo = true;
if (foo) {
{ // <-- explicit block
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError
for循环里用let声明循环变量时,循环变量的作用域不是整个for循环过程,而是每一次迭代都会重新生成一个新的循环变量,一次迭代结束之后,这个变量就会消亡,下一次迭代又会有一个新的同名循环变量生成。如code 7所示例子,code 8是它的等价代码。
/*-----------code 7----------*/
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError /*-----------code 8----------*/
{
let j;
for (j=0; j<10; j++) {
let i = j; // re-bound for each iteration!
console.log( i );
}
console.log( i ); // ReferenceError
}
const关键词
const也是ES6引入了一个新的关键字,const声明的变量会和let声明的变量一样拥有块作用域,除此之外,const声明的变量的值是不可改变的,任何想要修改const变量值的操作都会报错。
JS作用域延伸——变量、函数提升
JS代码中有一个神奇的现象,函数作用域或全局作用域中后申明的var变量和函数可以提前被调用,如code 9所示,这段代码等价于code 10,这种现象称为变量、函数提升,需要注意变量是只有其申明被提升,赋值以及其他可执行逻辑还是按照原先的顺序执行,函数是整体被提升,包括函数声明和函数内容。
/*-----------code 9----------*/
foo();
function foo() {
console.log( a ); // Undefined
var a = 2;
}
/*-----------code 10----------*/
function foo() {//函数提升
var a; //变量提升
console.log( a ); // Undefined
a = 2;
}
foo();
变量、函数提升现象时由JS代码运行的内部原理决定的。JS是一种解释型语言,依靠JS引擎对代码进行实时解释执行,JS自上而下的执行过程包括两个阶段:编译和执行,在编译阶段进行形参分析、变量函数申明,在执行阶段再顺序执行脚本语言。因为JS引擎是先进行变量函数申明再执行脚本,因此所有变量就好像是提到了它所在范围的最前端进行执行了一样。
关于变量函数提升还有几点需要注意。
表达式中的函数定义是不会被提升的,它只遵循变量提升的原则,如code 11所示,它等价于code 12;
/*-----------code 11----------*/
foo(); // not ReferenceError, but TypeError!
var foo = function bar() {
// ...
}; /*-----------code 12----------*/
var foo;
foo(); // not ReferenceError, but TypeError!
foo = function bar() {
// ...
};
函数提升的优先级高于变量,即如果同时定义了两个同名的变量和函数,函数会覆盖变量,如code 13,最终会打印出1而不是显示TypeError,code 14是它的等价代码。
/*-----------code 13----------*/
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
}; /*-----------code 14----------*/
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};
两个同名的函数定义,后出现的函数定义会覆盖前面的函数定义。如code 15等价于code 16。
/*-----------code 15----------*/
foo(); // 3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
} /*-----------code 16----------*/
function foo() {
console.log( 1 );
}
function foo() {
console.log( 3 );
}
foo(); // 3
foo = function() {
console.log( 2 );
};
JS默认只有函数作用域,因此代码块(if、for等)里面的函数定义也会被提升,如code 18等价于code 17。
/*-----------code 17----------*/ foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
} /*-----------code 17----------*/ function foo() { console.log("a"); }
function foo() { console.log("b"); }
foo(); // "b"
var a = true;
if (a) {
}
else {
}
let、const关键词声明的变量不存在变量提升现象。
延伸阅读:解释型语言和编译型语言的区别。
由于计算机无法直接执行高级语言,它只能识别二进制的机器码,因此高级语言一定要翻译成机器语言计算机才能识别运行。语言翻译的方式有两种:编译和解释,两者的主要区别是翻译语言的时间不同。按照语言翻译方式的不同,高级语言被分为编译型语言和解释型语言。
编译型语言需要在执行代码之前通过编译器将代码翻译成机器语言,生成.exe可执行二进制代码,依次编译可多次执行,因此执行效率比较高。比如:C/C++、Delphi、Pascal、Fortran。
解释型语言不需要提前编译,在执行的时候再通过解释器进行语言的翻译,因为每次执行都需要重新尽心语言的翻译,因此解释型语言的效率比较慢;但解释型语言的跨平台性比较好,只要在特定的平台安装对应的解释型就能运行代码。如JAVA、Basic、javascrip、Python。
JS作用域延伸——变量查询
上面提到,JS自上而下的执行过程包括两个阶段:编译和执行,在执行阶段大部分的语句都涉及变量查询,因此了解变量查询的概念是重要的。
根据查找的内容不同变量查询可分为LHS(left-Hand-Side)查询和RHS(right-Hand-Side)查询。根据字面理解,LHS表示要查找的变量处于“=”的左边,RHS表示要查找的变量处于“=”的右边。更加准确的说,LHS要查找的是存在变量的内存地址,RHS要查找的是变量的值。
变量查询的范围是变量的作用域链,作用域链由包含变量的从内到外的多个作用域组成(函数\块作用域>函数\块作用域>...>全局域),按顺序依次查找从内到外每个作用域,知道找到变量。
参考资料:
[1] You don't know js -- Scope & Closures
[2] 深入理解javascript作用域系列第一篇——内部原理
[3] 深入理解javascript作用域系列第二篇——词法作用域和动态作用域
[4] 深入理解javascript作用域系列第三篇——声明提升(hoisting)
[5] 深入理解javascript作用域系列第四篇——块作用域
[7] 什么是脚本语言?什么是解释性语言?什么是编译性语言?
JS基础学习——作用域的更多相关文章
- JS基础学习——闭包
JS基础学习--闭包 什么是闭包 闭包的定义如下,它的意思是闭包使得函数可以记住和访问它的词法范围,即使函数是在它声明的词法范围外执行.更简单来讲,函数为了自己能够正确执行,它对自己的词法范围产生闭包 ...
- JS基础学习——对象
JS基础学习--对象 什么是对象 对象object是JS的一种基本数据类型,除此之外还包括的基本数据类型有string.number.boolean.null.undefined.与其他数据类型不同的 ...
- JS 基础学习随想
2012年就已经接触过了js,给我的印象:这是一门谈不上复杂的语言.大概这就是所谓的学的越浅,用的越少,觉得自己会的东西好像得更多吧!开始做基础练习题的时候觉得好像都十分简单.可是后来在做到对象数组的 ...
- handlebars.js基础学习笔记
最近在帮学校做个课程网站,就有人推荐用jquery+ajax+handlebars做网站前端,刚接触发现挺高大上的,于是就把一些基础学习笔记记录下来啦. 1.引用文件: jquery.js文件下载:h ...
- JS基础学习1
1 JS 概述 一个完整的javascript实现是由以下3个不同部分组成的: (1) 核心(ECMAscript) (2) 文档对象模型(DOM) Document object ...
- JS基础学习篇(一)
近来一直在学习js和jquery.刚刚进入前端工作还没有多久,虽然大学里学习的是编程自认为也学的还可以,但前端接触的不多,一直认为前端十分简单.其实不然,特别是工作的时候要自己设计一个完整的项目前端, ...
- 两万字Vue.js基础学习笔记
Vue.js学习笔记 目录 Vue.js学习笔记 ES6语法 1.不一样的变量声明:const和let 2.模板字符串 3.箭头函数(Arrow Functions) 4. 函数的参数默认值 5.Sp ...
- Node.js基础学习四之注册功能
前言:在Node.js学习(二)和(三)中介绍了如何在Node.js 中获取登录的用户名和密码与数据库进行验证并返回数据给客户端 需求:实现注册功能 为了区分登录和注册是两个不同的请求,在端口后面加上 ...
- JS基础学习第五天
作用域 作用域简单来说就是一个变量的作用范围.在JS中作用域分成两种: 1.全局作用域 直接在script标签中编写的代码都运行在全局作用域中全局作用域在打开页面时创建,在页面关闭时销毁.全局作用域中 ...
随机推荐
- Udp 网络字节序
1.网络上的数据是一个字节一个字节的串行传递的. 2.字节序,规定,在内存里存储时,低字节在前称为小端,高字节在前称为大端,(现在主流系统都是小端的) 3.网络字节序,如果先传高字节,则是大端传输:如 ...
- 洛谷 P2096 最佳旅游线路
某旅游区的街道成网格状.其中东西向的街道都是旅游街,南北向的街道都是林阴道.由于游客众多,旅游街被规定为单行道,游客在旅游街上只能从西向东走,在林阴道上则既可从南向北走,也可以从北向南走. 阿龙想到这 ...
- 首字母变大写(stringstream的应用)
Problem Description 输入一个英文句子,将每个单词的第一个字母改成大写字母. Input 输入数据包含多个测试实例,每个测试实例是一个长度不超过100的英文句子,占一行. O ...
- 文件管理NSFileManager
//NSFileManager - (void)viewDidLoad { [super viewDidLoad]; NSLog(@"%@",NSHomeDirectory()); ...
- angularjs中向html页面添加内容节点元素代码段的两种方法
第一种方式:原生JS向html页面添加内容节点元素代码段: <!DOCTYPE html> <html> <head> <meta charset=" ...
- JAVA数据结构--希尔排序
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能.这样可以让一个元素可以一次性地朝最终位置前进一大步.然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需 ...
- Luogu P2243 电路维修 双端队列BFS
当转移的代价是0和一个分明不同的权值时,可以用双端队列BFS去跑(你跑最短路也没问题..QWQ) 而对于这道题,边旋转代价是1,不旋转代价是0:可以直接建图最短路,也可以跑BFS 这个题建图很有意思: ...
- Party All the Time(三分)
In the Dark forest, there is a Fairy kingdom where all the spirits will go together and Celebrate th ...
- HDU - 1285-确定比赛名次(拓扑排序+优先队列)
有N个比赛队(1<=N<=500),编号依次为1,2,3,....,N进行比赛,比赛结束后,裁判委员会要将所有参赛队伍从前往后依次排名,但现在裁判委员会不能直接获得每个队的比赛成绩,只知道 ...
- BFS和图的最短路径 279,127,126
在本题中,任何一个正整数都会由完全平方数1组成,所以不可能没有解. 贪心是不成立的,因为如果寻找12的完全平方数,使用贪心,则它由9,1,1,1四个数组成:但是最少的完全平方数是由三个4组成的. 4- ...