作用域和闭包:
创始人
2024-05-26 07:50:26
0

1、LHS和RHS查询

编译一段代码,需要js引擎和编译器(js引擎负责整个程序运行时所需的各种资源的调度,编译器只是js引擎的一部分,负责将JavaScript源码编译成机器能识别的机器指令,然后交给引擎运行)

编译的过程:分词/词法分析-->解析/语法分析-->代码生成

分词/词法分析:将一连串字符打断成有意义的片段,称为token,比如var a=2;分词的token有:var、a、=、2和;

解析/语法分析:编译器将一个token的流(数组)转换为一个抽象语法树(AST--Abstract Syntax Tree),它表示了程序的语法结构。

代码生成:编译器将上一步中生成的抽象语法树转换为机器指令,等待引擎执行。

执行:LHS(Left-hand Side)和RHS(Right-hand Side),是在代码执行阶段JS引擎操作变量的两种方式,区别在于LHS是赋值,而RHS是查询。

LHS和RHS获取变量的位置就是作用域。

2、什么是作用域?

作用域指程序中定义变量的作用域,它决定了当前执行代码对变量的访问权限。

全局作用域:程序的最外层作用域,一直存在。

函数作用域:函数作用域只有函数被定义时才会创建,包含在父级函数作用域/全局作用域中。

每段独立的执行代码块只能访问自己作用域和外层作用域中的变量,无法访问到内层作用域的变量。

3、作用域链:

当可执行代码内部访问变量时,会先查找本地作用域,如果找到目标变量即返回,否则会去父级作用域继续查找......一直找到全局作用域。把这种作用域的嵌套机制称为作用域链。

函数的参数也在函数作用域中。

4、词法作用域(静态作用域)与动态作用域

JavaScript使用的作用域类型。函数被定义的时候,它的作用域就已经确定了,和拿到哪里执行没有关系,因此词法作用域也被称为“静态作用域”。

var value = 1;function foo() {console.log(value);
}function bar() {var value = 2;foo();
}bar();// 结果是 ???

这段代码中,一共有三个作用域:全局作用域、foo的函数作用域和bar的函数作用域。

foo里面访问量本地作用域中没有定义的变量value,为了拿到这个变量要去foo的上层作用域中找。上层作用域是bar还是foo定义时的全局作用域呢?按照词法作用域中的定义,在foo被定义的时候,作用域已经被确定了,所以它的上层作用域是全局作用域,所以结果应当是1.

动态作用域:程序执行期间,对于一个名字x的使用,指向的是最近被调用但还没有终止且声明了x的过程中的这个声明。

var value = 1;function foo() {var  value=3console.log(value);
}function bar() {var value = 2;foo();
}bar();// 结果是 3

总结:词法作用域(静态作用域)是关联在编译期间的,对于函数来说就是函数定义的位置决定了该函数所属范围;动态作用域是关联在程序执行期间的,对函数来说就是函数执行的位置决定了函数的所属的范围。

5、块级作用域

{....块级作用域}

注意用var定义的变量可以在块级作用域外访问到‘ES6使用let和const代替var关键字,来创建块级作用域。

if(true){
var a=1
}
console.log(a)//1if(true){
let a=2
}
console.log(a)//Reference

6、创建作用域

(1)函数作用域

function f(){//f的函数作用域
}

(2)块级作用域(使用let和const创建)

for(let i=0;i<4;i++){console.log(i)
//i的块级作用域
}

(3)块级作用域(使用try...catch...)

try{undefined()//强制产生异常
}catch(err){console.log(err)//TypeError:undefined is not a function
}
console.log(err)//err is not defined

(4)使用eval欺骗词法作用域(性能问题,不推荐)

function foo(str,a){eval(str)console.log(a,b)
}
var b=1
foo('var b=2',1)//1 2
eval(str)即eval(var b=2)会被当作在foo中有语句var b=2来执行,所以相当于在函数foo内部创建了一个变量b,所以打印出的b为2

但是在严格模式下,eval(...)有着自己的词法作用域,其中的声明无法修改其中的作用域:

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

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

(5)with(性能问题,不推荐)

eval(..) 函数如果接受了一个含有一个或多个声明的代码,就会修改其所处的词法作用域,而 with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。

function foo(obj){with(obj){a = 2;}
}var o1 = {a:3
};
var o2 = {b:3
};
//分别定义了o1和o2两个对象,o1中只有属性a,o2中只有属性bfoo(o1);
console.log(o1.a); //2
//with会单独创建自己的作用域,在o1中找到了属性a,a=2即将值2赋给了属性afoo(o2);
console.log(o2.a); //undefined
//with创建独属于自己的作用域o2,o2中没有属性a,然后在上层作用域中查找,也没有找到变量a,所以会单独创建全局变量a,并赋值为2(此时是正常的 LHS 表示法查找)
console.log(a); // 2 ---不好,a 被泄露到全局作用域上了

总结:

JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..) 和 with。前者可以对一段包含一个或多个声明的“代码”字符串转换为动态作用域,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

7、作用域的应用场景

模块化:

立即使用函数表达式(Immediately Invoked Function Expression简写IIFE)

//module1.js
(function(){var a=1console.log(a)
})();//module2.js
(function(){var a=2console.log(a)
})()(function(global){if(global...){//is browser}else if(global...){//is nodejs}
})(window)

8、闭包

能够访问其他函数内部变量的函数称为闭包。闭包就是函数内部定义的函数,被返回了出去并在外部调用。闭包的执行看起来是绕过了作用域的监管机制,从外部也能获取到内部作用域的信息。

8.1、闭包的应用场景(需要维护内部变量)

单例模式:

一个类只能有一个实例。实现方式是先判断实例是否存在,如果存在直接返回该实例,不存在则返回新创建的实例。单例的好处是避免重复实例化带来的内存开销:

function Singleton(){this.data='singleton'
}
Singleton.getInstance=(function(){var instance;return function(){if(instance){return instance}else{instance=new Singleton()return instance}}
})();
var sa=Singleton.getInstance()
var sb=Singleton.getInstance()
console.log(sa===sb)//true
console.log(sa.data)//'singleton'

模拟私有属性

function getPrivateAttribute(){var _name='John'var _age=24return function(){return {getName:function(){return _name},getAge:function(){return _age}}}
}
var obj=getPrivateAttribute()()//返回了{getName:function(){return _name},getAge:function(){return _age}}
console.log(obj.getName())//John
console.log(obj.getAge())//24
console.log(obj._age)//undefined            

柯里化

把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数。

常见的bind方法就是用柯里化的方法来实现:

Function.prototype.myBind=function(context=window){if(typeof this!=='function')throw new Error('Error')let selfFunc=thislet args=[...arguments].slice(1)return function F(){//因为返回了一个函数,可以new F(),所以需要判断if(this instance of F){return new selfFunc(...args,arguments)}else{//bind可以实现类似这样的代码f.bind(obj,1)(2),所以需要将两边的参数拼接起来return selfFunc.apply(context,arg.concat(arguments))}}
}            

8.2、闭包带来的问题

内存泄露(详见JavaScript内存泄露和垃圾回收机制)

9、总结

(1)JavaScript语言层面原生支持:全局作用域、块级(函数)作用域:全局作用域程序运行就有,函数作用域只有定义函数的时候才有,它们之间是包含的关系。

(2)词法(静态)作用域和动态作用域:静态作用域指函数定义的时候就确定了作用域,动态作用域指函数执行的时候才会确定。eval(str)和with(obj)会欺骗词法作用域,eval(str)会将str作为语句执行,with则是创建独属于obj的作用域,如果找不到会继续向上层作用域查找,找不到则会创建全局变量,找到则重新给变量赋值。因此evel和with造成引擎无法在作用域查找时进行优化,造成性能问题,不推荐使用。

(3)作用域之间是可以嵌套的,嵌套关系称为作用域链。

(4)可执行代码在作用域中查询变量时,只能查询本地作用域及上层作用域,不能查找内部的作用域。JS引擎搜索变量时,会先查询本地作用域,找到立即返回,找不到会去上层作用域中查找,层层往上,直到全局作用域。

(5)有权访问另一个函数内部的函数,称为闭包。闭包的本质是利用了作用域的机制,来达到外部作用域访问内部作用域的目的。缺点是可能造成内存泄露。

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...