作用域是当前的执行上下文,在其中的值和表达式“可见”(可被访问)。作用域也可以堆叠成层次结构(作用域链),子作用域可以访问父作用域,反之不行

在书籍《你不知道的 Javascript》上卷中有这样一句话:作用域是根据名称查找变量的一套规则

所以通俗来讲,作用域在 JavaScript 里可以理解为:作用域是一套规则,可以决定一个变量或函数的有效访问范围

静态词法作用域

解释:

  • 静态:代码书写的位置
  • 词法:通过关键字声明的语法变量,如:varletconstclassfunctionimportexport
  • 作用域:代码中定义变量的区域,它决定了变量的可见性和生命周期,作用域层层嵌套是一个树状结构从叶子到根节点依次查询访问

静态词法作用域是 JavaScript 中确定变量访问权限的一种机制。在这种机制下,一个变量的作用域在定义时就已经确定了,而不是在运行时才确定

全局作用域

脚本模式运行所有代码的默认作用域

var a = 'a'
window.b = 'b'
 
c = 'c' // 不推荐的写法,非严格模式下相当于: globalThis.c = 'c'
 
function bar() {
  // 函数中没用 var 声明的变量,其作用域也是全局的
  d = 'd' // 不推荐的写法,和 c 一样,非严格模式下相当于: globalThis.d = 'd'
}
bar()
 
// --------------------------------------------
 
delete a // false -> 变量不能删除
 
// c 不是严格意义的全局变量,而是全局属性
delete c // true -> 相当于 delete globalThis.c
 
// 如何删除 a?
delete window.a // true -> window 的属性 a 可以被删除

模块作用域

ESModule 模块模式中运行代码的作用域。在一个模块中定义的变量,除非显式地导出(export),否则其他模块不能访问

函数作用域

由函数创建的作用域,也称局部作用域

function bar() {
    var a = 'a'
 
    function foo() {
        var b = 'b'
        console.log(a) // 'a'
    }
    
    console.log(b) // Error: undefined
}
bar()
 
console.log(a) // Error: undefined

函数内部的变量如何暴露给外界?

  1. return
  2. 闭包

块级作用域

用一对花括号(一个代码块)创建出来的作用域

ES6 新增,使用 letconstcalss 声明的变量拥有块级作用域,严格模式的 function 也支持块级作用域(详见:变量与函数声明

ES6 之前没有块级作用域,这是语言设计缺陷

// ES6 之前:没有块级作用域
while (var a = 'a') {
    var b = 'b'
}
console.log(a, b) // 'a' 'b'
 
// ES6 之后:有块级作用域(通过 let 和 const)
while (let c = 'c') {
    const d = 'd'
}
console.log(c, d) // Error: undefined undefined
 
{ // 花括号就是一个块
    const e = 'e'
}

this:动态执行上下文

函数的 this 关键字是在函数被调用时确定的,而不是在函数被定义时确定的。这就是所谓的“运行时绑定”

this 指向问题:

普通函数

普通函数:谁调用,this 指向谁

  • function 中的 this
const fn = function() {
    console.log(this)
}
 
const obj = {
    name: 'OBJ',
    fn,
    subObj: {
        name: 'SUB_OBJ',
        fn,
    },
}
 
fn() // window,严格模式下是 undefined
// 这里不能理解为 window.fn()
// 因为用 const/let 定义的变量不会被挂载到 window 上
// 即:
window.fn // undefined
 
obj.fn() // obj {}
obj.subObj.fn() // subObj {}
 
// this: 执行主体
// 可以理解为谁把函数执行的(谁调用的),函数内部的 this 就指向谁
// 和函数在哪定义、在哪执行、在哪调用都没有关系
  • 回调函数中的 this

给当前元素的某个事件行为绑定方法,当事件行为触发,方法中的 this 是当前元素本身(排除 attachEvent

document.body.addEventListener('click', function() { console.log(this) }) // body
// 可以理解为 body 调用了回调函数

构造函数/类

构造函数/类中的 this 指向当前类的实例

function Dog(name) {
    this.name = name
}
 
const wangcai = new Dog('旺财')
wangcai.name // '旺财'

箭头函数

箭头函数中没有自己的 this,所用到的 this 都是其所处上下文(静态词法作用域)中的 this

const demo = {
    name: 'DEMO',
    fn() {
        console.log(this) // demo
 
        // setTimeout 的回调函数是 window 调用的,所以 this 指向 window
        setTimeout(function() { console.log(this) }, 0) // window
        
        // 箭头函数的 this 是看在哪定义的
        // 这里在 fn 中定义,this 就是 fn 的 this
        setTimeout(() => console.log(this), 0) // demo
    }
}
demo.fn() // 上面的注释都基于此调用

显式改变 this 指向

通过 Function.prototype.call/apply/bind 显式改变 this 指向

function fn(x, y) {
    console.log(this, x, y)
}
 
fn.call(obj, 10, 20) // obj
fn.apply(obj, [10, 20]) // obj
document.body.addEventListener('click', fn.bind(obj, 10, 20)) // obj

手写 call、apply、bind

箭头函数无法显式改变 this 指向

箭头函数中没有自己的 this,所以无法通过 Function.prototype.call/apply/bind 显式改变其 this 指向

底层:箭头函数的底层是 bind,无法改变 this,只能改变参数