javascript中的作用域和作用域链

106 阅读4分钟

作用域

作用域控制变量的访问范围、声明周期。分为全局作用域和局部作用域。局部作用域可以访问全局作用域的变量,但反过来不行。 在javascipt中,变量的作用域有全局作用域、函数作用域和块级作用域。

一、全局作用域

以下两种方式声明的都是全局变量,可以直接在全局作用域中使用。

var a = 10;
function test() {
  console.log(a);
}
window.test(); // 10

省略var关键字声明的变量是全局变量.

function test() {
  a = 10;
  console.log(window.a);
}
test(); // 10

所有的全局变量都挂载到window对象上,可以通过window.变量名访问,其中还包含一些浏览器内置的全局变量,如window.onload、window.setTimeout、window.alert等。

console.log(window.innerHeight); // 窗口高度
console.log(window.innerWidth); // 窗口宽度
console.log(window.location); // 页面地址 

二、函数作用域

var a = 10;
function test() {
  var b = 100;
  console.log(a,b); // 10,100
}
console.log(b); // ReferenceError: b is not defined

三、块级作用域 在es6之前使用var 声明的变量只有函数作用域和全局作用域,而es6中引入了let和const关键字可用于声明块级作用域的变量,它使用{}来定义一个块级作用域,例如for(){}、if(){}、switch(){}等。 块级作用域中声明的变量只能在该作用域中使用,且不存在变量提升,解决了变量覆盖的问题。

for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
} // 0 1 2 3 4

if(false){
    let a = 10;
}

{
    let a = 20;
    console.log(a); // 20
}
console.log(a); // ReferenceError: name is not defined

四、函数和变量的声明提升 使用var关键字声明的变量和函数会进行提升,变量声明提升,函数声明整体提升。

 console.log(c); // undefined
 console.log(f()); // fn
 var c = 20;
 function f(){
    return 'fn'
 }

五、如何访问函数作用域中的变量?

  • 保存为全局变量
 function fn(){
  window.a = 10;
}
  • 函数返回值
function fn2(){
   var a = 10;
   return a;
 }
  • 闭包
 function fn3(){
   var a = 10;
   return function(){
      return a;
   }
 }

 var func = fn3();
 console.log(func()); // 10

作用域链

作用域链是由一系列变量对象组成的链条,它决定了变量的查找方式。当在某个作用域中访问一个变量时,会先在当前作用域中查找,如果没有找到,则逐级向上查找,直到全局作用域。

var a = 10;
function test() {
  var b = 100;
  function inner() {  
    console.log(a,b); // 10,100
  }
  inner();
}
test();
  1. 首先,在test()函数中,声明了变量b,它位于test()函数的作用域中。
  2. 然后,在test()函数中调用了inner()函数,inner()函数位于test()函数的作用域中。
  3. 进入inner()函数,首先查找变量a,它位于inner()函数的作用域中,找到了a,返回10。
  4. 然后查找变量b,它位于inner()函数的作用域中,找到了b,返回100。
  5. 最后,打印a和b的值,结果为10和100。

词法作用域和动态作用域

在javascipt中,变量的作用域采用的是词法作用域(静态作用域),即变量的声明的位置决定了它的作用域,与之相反的是动态作用域即变量的作用域是由运行时环境决定的.

 var count = 10
 function fn4(){
   var count = 20;
   fn(5)
 }

 function fn5(){
   console.log(count);
 }
 console.log();// 10

上面的例子如果是静态作用域输出10,如果是动态作用域输出20。

在javascript中,this指向的作用域就是动态的,它总是指向函数的直接调用者。

 // this的执行是动态的
 const student = {
   name: 'Tom',
   sayName: function(){
     console.log(this.name);
   }
}

const teacher = {
   name: 'Jane',
}
console.log(student.sayName()); // Tom
console.log(student.sayName.call(teacher)); // Jane

拓展

对于下面的代码你有几种方式得到正常的输出结果?

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i); // 5 5 5 5 5
  }, 0);
}
  • 自执行函数(IIFE),将每次循环的i保存到函数作用域中
for (var i = 0; i < 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i); // 5 5 5 5 5
    }, 0);
  })(i);
}
  • 使用函数参数,将每次循环的i保存到函数作用域中
for (var i = 0; i < 5; i++) {
  timeout(i);
}

function timeout(i) {
  setTimeout(() => {
    console.log(i)
  }, 0);
};
  • 使用let关键字声明变量,将每次循环的i保存到块级作用域中
for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i); // 0 1 2 3 4
  }, 0);
}
  • 使用async
function timeout(time) {
  return new Promise(function(resolve, reject){
    setTimeout(() => {
      resolve();
    }, time);
  })
}

async function fn() {
  for (var i = 0; i < 5; i++) {
    await timeout(0);
    console.log(i); // 0 1 2 3 4
  }
}

fn();
  • 使用setTimeout的第三个参数, 本质上也是将循环过程中的i保存到函数作用域中。
for (var i = 0; i < 5; i++) {
  setTimeout((i) => {
    console.log(i); // 1 2 3 4 5
  }, 0, i);
}