你不知道的javascript-------(一)作用域/变量提升

43 阅读5分钟

1.LHS和RHS

概念定义:LHS和RHS的含义是‘赋值操作的左侧或者右侧’,并不意味着就是“=”赋值操作符的左侧或者右侧。赋值操作符还有其他几种形式,因此在概念上最好理解为“赋值操作的目标是谁(LHS)”和“谁是赋值操作的源头(RHS)”。

function foo(a){
  console.log(a)
}
foo(2)//此处存在隐式的赋值操作,a=2

在上述代码中:

  • 存在隐式的赋值操作,即foo中的a=2,此时会进行LHS查找,观察是否已经声明了a这个变量,如果存在则进行赋值操作。(赋值操作)
  • 在console.log操作中会对a进行RHS查找,观察是否在之前就对a进行过赋值操作,取到a的值。(取值操作)

测试题:中出其中所有的LHS查询(3处)和RHS查询(4处)。

function foo(a){
var b = a
return a + b
}
var c = foo(2)
LHS查询:c =..., a=2, b=...
RHS查询:...foo(), =a, a, b

2.词法作用域

无论函数在哪里被调用,也无论他如何被调用,他的词法作用域都只由函数被声明的位置决定。(要在声明的位置去找)

function bar(){
  console.log(myName)
}
function foo(){
  var myName = "qqq"
  bar()
}
var myName = 'ppp'
foo()//这里会打印‘ppp’,因为从bar找myName找不到,就从bar声明的地方向外寻找,找到myName = 'ppp'

稍微复杂一点的。(需要好好寻找作用域链)

var bar = {
  myName:"oooo",
  printName:function(){
    console.log(myName)
  }//printName是在变量中声明的而不是函数中,因此他的词法作用域是全局作用域
}
function foo(){
let myName = 'ppp'
return bar.printName
}
let myName = 'llll'

let _printName = foo() //这个时候的_printName是一个在全局作用域中声明的崭新的函数。
_printName() //根据作用域链找到llll
bar.printName()//根据作用域链找到llll

“欺骗”词法作用域的方法:eval(),with()。(这两种方法非严格模式下可以正常使用,在严格模式下会有影响,with()被完全禁止,在保留核心功能的前提下,间接或者非安全的使用eval()也被禁止了。)

  1. eval():eval():接收一个字符串作为参数,参数中的内容会被视为在eval()所在的位置进行执行。
function foo(str,a){
  eval(str)//相当于在此处执行了b=3的操作。因此在输出的时候,b是3而不是2。
  console.log(a,b)
}
var b = 2;
foo('b=3',5) //5,3

2.with()

function foo(obj){
  with(obj){
    a = 2
  }
}
var o1 = {
  a:1
}
var o2 = {
  b:1
}
foo(o1)
console.log(o1.a)//2

foo(o2)
console.log(o2.a)//undefined
console.log(window.a)//a

上述代码输出解释:

  • 当我们传递o1给with的时候,with所声明的作用域是o1,而这个作用域中含有一个同o1.a的标识符,所以修改o1中a的值。
  • 当我们传递o2给with的时候,将o2作为作用域,其中并没有a标识符,因此进行了正常的LHS标识符查找。(o2中没有,就去全局中寻找,没有寻找到,就自动创建了一个全局变量。)

3. 函数作用域

立即执行函数表达式(LIFE/Immediately Invoked Function Expression)

第一种写法
(function(){...}())
第二种写法
(function(){...})()
区别在于后面的括号有没有括进去,两种写法功能一致。

4. 块作用域

块级作用域的例子:

  1. try/catch
try{
  undefined()
}
catch(err){
  console.log(err)//这里会正确的输出,catch创建了一个块级作用域,其中声明的部分只在catch中有效
}
console.log(err)//这里会报错,因为没有err

2.let/const

  • let和var的区别中有:let存在块级作用域,而var没有,var存在变量提升。
//对于var
var foo = true,baz = 10;
if(foo){
  var bar = 3

}
console.log('bar',bar)//这里会正确打印bar的值,bar,3。var会进行变量提升,然后赋值,就导致能正确的打印出bar的值。
  if(baz > bar){
    console.log('baz',baz)
  }
  
  //对于let
  var foo = true,baz = 10;
if(foo){
  let bar = 3

}
console.log('bar',bar)//这里会报错,就是因为let的块级作用域。ReferenceError: bar is not defined
  if(baz > bar){
    console.log('baz',baz)
  }


  • const同let一样存在块级作用域,区别在于const声明的时候必须赋值,同时赋值之后不能进行修改。
  const b = 4
  b=5 //这里就会报错。TypeError: Assignment to constant variable.
  console.log('b',b)
块作用域的替代方案
{
  let a = 2
  console.log(a) //2
}
console.log(a) //ReferenceError: a is not defined

要实现这种效果可以使用try/catch

try{
  throw 2
}catch(a){
  console.log(a) //2
}
console.log(a) //ReferenceError: a is not defined

5. 提升

造成提升的原因:在代码执行之前会先进行编译,在编译的时候包括变量和函数的声明都会在执行之前首先被处理。(只有声明本身被提升,赋值或者其他逻辑操作会留在原来的位置。)

var a = 2

上面的代码可以被看做分成两部执行

var a //在编译的时候执行
a = 2 //在正常的赋值阶段执行。

因此分析下面的问题

示例问题:
a = 2
var a 
console.log(a) // 2

实际上是相当于
var a //编译阶段
a = 2 //执行阶段
console.log(a)

注意点

  1. 函数声明会被提升,函数表达式不会提升。
//函数声明
foo()
function foo(){
  console.log("执行函数")
}

//函数表达式
bar()
var bar = function foo(){
  console.log("执行函数")
}
//会报错:TypeError: bar is not a function。
//可以看到报错信息是TypeError,而并不是ReferenceError,这是因为在执行bar()的时候,bar的值是undefined
//上述代码可以分解成
var bar
bar()//所以这里不能当做函数执行。
bar = function foo(){
  console.log("执行函数")
}
  1. 函数会被首先被提升,然后才是变量。
foo() //function
var foo 
function foo(){
  console.log("function")
}
foo = function(){
  console.log('variable')
}

5. 闭包

简单了解闭包问题。 juejin.cn/post/717877…