浅谈javascript
今天我就详细整理一下js作用域的知识来疗疗我被捶打的经历!! 首先我们要清楚计算机不能识别编程语言,计算机唯一能识别的就是二进制数据。在计算机通信中我们电脑输入的指令其实就是一串串二进制的数据(100010001100)。这时候就需要将javascript代码进行编译和转译。JS是一种解释型语言,这意味着它通常不是在编译成机器码之后再执行,而是在代码被加载时直接由浏览器或其他JavaScript引擎(如Node.js)解释并立即执行。java是交给服务器运行,前端(javascript)代码给客户端(浏览器)用。浏览器能读的懂我们写的js代码。我们以google浏览器为例子,如果把浏览器理解为一家企业的话这家企业就会有许多部门:如人力资源部门、编程部门、x销售部门......其中有一个js执行引擎部门。我们以Google浏览器的执行引擎为例-v8引擎。
词法分析
var a = 2
这段代码中我们定义了一个值赋值为2,在v8引擎里首先会进行词法分析。以这个例子为例会分析出词法单元:var,a,=,2。
解析
词法分析结束后就会将词法单元转化为一个逐级嵌套的程序语法结构树 --抽象语法树
生成代码
之后会重新生成一份代码。var a=2 此时v8会读取我们最后生成出来的代码。
我们以下面的代码来感受一下代码
function foo(a){ console.log(a+b) } var b=2 foo(1)
执行结果为3,我们把代码修改一下看看又会有怎样的效果呢?
function foo(a){ console.log(a+b); } function bar(){ var b=2 } bar() foo(1)
代码直接报错了!!
有效标识符
function foo(a){ var b=2 console.log(a+b) } a,b是foo的有效标识符
我们也可以这样写代码
function foo(a){ var b=2 function bar(c){ console.log(a+b+c) } bar(3) } foo(1)
首先呢a,b,bar是foo的有效标识符,c为bar的有效标识符。打印出来的结果为6。从这里我们可以看到这么一个问题foo的调用带来了bar的声明,foo要是不调用v8都不知道有bar这个东西,所以bar是foo的有效标识符是foo域里边的东西。那么我们可以说foo的域是bar的外层域。所以从效果来看内层领域能访问外层领域。
作用域
内层作用域是可以访问外层作用域的。反之外层作用域是不能访问内层作用域的
var a=1 function foo(){ console.log(a); } foo()
a声明在全局作用域,因此可以打印出a=1
function foo(){ var a=1 } foo() console.log(a);
而这段代码中a是声明在foo里边的,属于内层作用域。而console.log(a)是在外层作用域中。因此会报错。
我们看下这段代码!!
var b; function foo(a){ b=a+bar(a*2) console.log(b*3) } function bar(a){ return a-1 } foo(2)
打印结果为15如何让这段代码更加优雅呢? bar是为foo里边的逻辑来提供服务的,也可以称为foo里边的逻辑走到某一步突然,需要一个辅助函数(bar)来干点啥事情。
function foo(a){ var b; function bar(a){ return a-1 } b=a+bar(a*2) console.log(b*3); } foo(2)
这样就封装的很彻底,不零散。foo要实现的功能要用到的元素隐藏在foo的内部并没有放到外部。没有影响到全局。我们试想一下假如企业多人开发写一个项目,我要实现一些功能在全局里边疯狂定义一些变量,这时候同事也要实现一些功能也在全局定义一些变量,这时候可能会出现定义的变量名会出现重名,会有覆盖情况。Bug~也就随之出现。
var a=1 function foo(){ var a=2 console.log(a); } foo()
打印结果为2,访问foo自己本身形成的作用域,不会访问外部的。 我们来剖析一下,作用域分为两种一种是全局的一种是函数自己本身形成的。这份代码在v8执行的时候,v8看见这份代码了,v8会维护出来一个调用栈(先进后出),把代码放到调用栈里边整理。v8从上往下执行的时候第一个碰到的域就是全局作用域(全局上下文),之后呢foo调用了就形成了函数作用域,这时候就又会来一个foo执行上下文入栈,然后在foo里边找有效标识符,后边就开始console.log(a)的执行。console.log会到foo的执行上下文里边找a的值,找到了就不会找了,如果没找到的话就会继续往下找,到全局执行上下文里边找(这个过程跟栈先进后出的特点重合)。这就是内层作用域能访问外层作用域的原因。
自执行函数
自执行函数是不需要调用就能自动触发的函数
var a=2 (function(){ var a=3 console.log(a); })() console.log(a);
打印为3,2 或者
!function(){ }()
块级作用域
首先呢我们看看下边的代码
console.log(a); var a=1
var声明的变量会变量声明提升console.log打印出来的为undefined,于是es6打造了一个新的关键字为let,用let定义一个变量效果跟var基本一致。
console.log(a); let a=1
这样就会直接报错,变量声明提升的问题就直接干掉了。顺着这条路我们可以整理出: 1.var 存在声明提升,let不存在 2.let 会和{}形成块级作用域 3.var 可以重复声明变量let 不可以
我们可以看以下代码来了解下let和{}形成块级作用域的实际效果
{ let x = 5; if (true) { let x = 10; // 这里的x是另一个块级作用域内的变量 console.log(x); // 输出 10 } console.log(x); // 输出 5 } console.log(x); // 报错:ReferenceError: x is not defined
由此可见let和{}会形成块级作用域,此时由于外层不能访问内层作用域因此报了错误。 我们接下来聊聊let造成的暂时性死区问题
let a=1 if(1){ console.log(a); let a=2 }
出现直接报错的情况,这就是let独有的语法暂时性死区,按理来说会打印出1,但就是因为有暂时性死区这个语法会出现报错。if语句形成的作用域中已经申明了let a=2,这时候就会触发暂时性死区机制导致console.log不能访问外层作用域申明的a=1,因此直接报错。
我们接下来来聊聊两种特殊的情况。
欺骗词法作用域
function foo(str,a){ eval(str) console.log(a,b)//1,2 } var b=2 foo('var b=3',1)
打印出的结果为 1,3 eval(str)就相当于//var b=3 eval()将原本不属于这里的代码变得就像是天生定义在这里一样
var obj={ a:1, b:2, c:3 } obj.a=2 obj.b=3 obj.c=4 console.log(obj)
当对象里边的属性很多的时候这样改其实很麻烦,我们可以用with()语句来改
with(obj){ a=3 b=4 c=5 } console.log(obj)
打印为{a:3,b:4,c:5}
function foo(obj){ with(obj){ a=2 } } var o1={ a:3 } var o2={ b:3 } foo(o1) console.log(o1); foo(o2) console.log(o2); console.log(a)
打印出来结果为{a:2},{b:3},2。由此我们可以看出with(){}用于修改一个对象的属性值,但如果修改的 属性在原对象中不存在,那么该属性就会被泄露到全局。