Skip to content

作用域

什么是作用域

  • 在JavaScript中,作用域是变量可用性的代码范围,它决定了变量、函数、其他代码元素的生命周期和可见性。即作用域就是变量起作用的范围和区域。
  • 作用域存在目的是隔离变量保证不同作用域下同名变量不会冲突,是帮助我们组织和管理代码的一套规则。
  • 作用域可以分为三种:全局作用域、函数作用域和块级作用域。

全局作用域

  • 全局作用域在浏览器中是window对象,在node中是global对象,在worker中是self。

  • 全局作用域是作用域的根节点,在程序运行的任何期间任何位置都能被访问。

  • 全局作用域对象及属性(全局变量)始终都不会被垃圾回收,运行引擎会认为他时刻有效。

拥有全局作用的三种情况:

  1. 所有定义在最外的层函数和变量拥有全局作用域。
  2. 所有window对象的属性拥有全局作用域。
  3. 所有末定义直接赋值的变量。

    它会进行变量提升,然后声明为拥有全局作用域的变量。ES标准中不允许

函数作用域

  • 在函数内部定义的变量,拥有函数作用域。
  • 函数调用时创建,调用结束作用域随之销毁。(当产生闭包时,不会被销毁,可以利用闭包做数据持久性)
  • 每调用一次产生一个新的作用域,之间相互独立。(利用闭包做数据独立性)

块级作用域es6+

  • 块级作用域是指在一对花括号 { } 内声明的变量只能在这对{ }内访问。
  • 块级作用域是ES6引入的特性,使用 let 和 const 关键字声明的变量具有块级作用域,增强了变量的作用范围控制和代码的可维护性。
  • 块级作用域可以出现在函数、循环、条件语句等代码块中。
javascript
if (true) {
  let a = 5;
  const b = 10;
  console.log(a); // 输出: 5
  console.log(b); // 输出: 10
}
console.log(a); // 报错: a is not defined
console.log(b); // 报错: b is not defined
if (true) {
  let a = 5;
  const b = 10;
  console.log(a); // 输出: 5
  console.log(b); // 输出: 10
}
console.log(a); // 报错: a is not defined
console.log(b); // 报错: b is not defined

作用域链

  • 当代码在一个作用域中需要访问某个变量时,JavaScript引擎会沿着当前作用域由内向外逐级向上查找,直到找到该变量或到达全局作用域为止,这种作用域逐级关联的关系链就是作用域链。

    作用域链是解决变量可见性和访问性问题的机制。
    作用域链由多个执行上下文的变量对象组成,形成链表结构。
    作用域链在嵌套函数和闭包中非常有用。

词法作用域

  • 词法作用域作用域模型中的一种。作用域本质是一套规则,JavaScript遵循的就是词法作用域模型。(还有一种是 动态作用域,相对“冷门”)
  • 词法作用域又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是书写代码时的作用域。
javascript
var num = 10;
function fn1(){
  console.log(num)
}
function fn2(){
  var num  = 20;
  fn1()
}
fn2(); // 10 根据书写时的位置向上查找
var num = 10;
function fn1(){
  console.log(num)
}
function fn2(){
  var num  = 20;
  fn1()
}
fn2(); // 10 根据书写时的位置向上查找
可以使用 eval 和 with 函数修改词法作用域

eval:是一个非常危险的方法。永远不要使用 eval!

  • eval函数入参是一个字符串。当eval拿到一个字符串入参后,它会把这段字符串的内容当做js代码,插入到自己被调用的那个位置。
javascript
  function f1(str){
    eval(str); // 此处相当于 var num = 20
    console.log(num);
  }
  
  var num = 10;
  var str ="var num = 20"
  f1(str)
  function f1(str){
    eval(str); // 此处相当于 var num = 20
    console.log(num);
  }
  
  var num = 10;
  var str ="var num = 20"
  f1(str)

with:已弃用

  • with函数是引用对象的一种简写方式。当我们去引用一个对象中的多个属性时,可不用重复引用对象本身。
  • with 会原地创建一个全新的作用域,这个作用域内的变量集合,其实就是传入 with 的目标对象的属性集合。
  • “创建” 这个动作,是在 with 代码实际已经被执行后发生的,因此with实现了对书写阶段就划分好的作用域进行修改。
javascript
function fn(obj){
  with(obj){
    a = 2
  }
}
var f1 = {a:3}
var f2 = {b:3}
fn(f1)
console.log(f1.a) // 3
fn(f2)
console.log(f2.a)  // 输出undefined 
console.log(a) // 2
function fn(obj){
  with(obj){
    a = 2
  }
}
var f1 = {a:3}
var f2 = {b:3}
fn(f1)
console.log(f1.a) // 3
fn(f2)
console.log(f2.a)  // 输出undefined 
console.log(a) // 2

闭包

闭包(Closure)是指一个函数能够记住并访问它的词法作用域,即使这个函数在其词法作用域之外执行。

闭包的特性

  1. 函数嵌套:闭包通常是通过在一个函数内部定义另一个函数来创建的。
  2. 词法作用域:闭包能够记住它被创建时的词法作用域,并且可以访问该作用域中的变量。
  3. 持久化:闭包可以持久化其词法作用域中的变量,即使外部函数已经执行完毕。

数据隐藏和封装

javascript
function createCounter() {
  let count = 0;
  return {
    increment: function() {
      count++;
      console.log(count);
    },
    decrement: function() {
      count--;
      console.log(count);
    }
  };
}
const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
counter.decrement(); // 输出: 1
function createCounter() {
  let count = 0;
  return {
    increment: function() {
      count++;
      console.log(count);
    },
    decrement: function() {
      count--;
      console.log(count);
    }
  };
}
const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
counter.decrement(); // 输出: 1

count变量是私有的,只能通过increment和decrement方法访问和修改。


函数工厂

javascript
function createAdder(x) {
  return function(y) {
    return x + y;
  };
}

const add5 = createAdder(5);
console.log(add5(2)); // 输出: 7
console.log(add5(10)); // 输出: 15
function createAdder(x) {
  return function(y) {
    return x + y;
  };
}

const add5 = createAdder(5);
console.log(add5(2)); // 输出: 7
console.log(add5(10)); // 输出: 15

createAdder函数返回一个闭包,该闭包记住了x的值,从而可以创建不同的加法函数。