作用域与闭包

94 阅读4分钟

作用域、闭包是一个头疼的玩意,特别对应初次接触者来说真可谓是头疼,作为面试他也是面试题中的常青树。

题目

  • this 的不同应用场景

  • 创建 10 个<a>标签,点击的时候弹出来对应的序号

    js

    // 创建 10 个`<a>`标签,点击的时候弹出来对应的序号
    let i, a
    for (i = 0; i < 10; i++) {
        a = document.createElement('a')
        a.innerHTML = i + '<br>'
        a.addEventListener('click', function (e) {
            e.preventDefault()
            alert(i)
        })
        document.body.appendChild(a)
    }
    
  • 实际开发中闭包的应用

  • 手写 bind 函数

知识点

作用域

所谓作用域,即一个变量的合法使用范围

js

let a = 0
function fn1() {
    let a1 = 100

    function fn2() {
        let a2 = 200

        function fn3() {
            let a3 = 300
            return a + a1 + a2 + a3
        }
        fn3()
    }
    fn2()
}
fn1()

作用域分类

  • 全局作用域:在全局定义的变量,全局都可用,像 document
  • 函数作用域:在某个函数中定义的变量,只能用于当前函数,像 a b
  • 块级作用域(ES6):只能活跃于当前的块,示例如下

js

// ES6 块级作用域
if (true) {
    let x = 100
}
console.log(x)  // 会报错

自由变量

  • 一个变量在该作用域没有被定义,但被使用
  • 向上级作用域去寻找该变量,层层往上找 —— 如上面的示例

js

let a = 0
function fn1() {
    let a1 = 100

    function fn2() {
        let a2 = 200

        function fn3() {
            let a3 = 300
            return a + a1 + a2 + a3
        }
        fn3()
    }
    fn2()
}
fn1()

闭包

闭包 —— 作用域应用的一个特殊情况。一般有两种书写形式:

  • 函数作为参数被传入
  • 函数作为返回值

js

// 函数作为返回值
function create() {
    let a = 100
    return function () {
        console.log(a)
    }
}
let fn = create()
let a = 200
fn()

// 函数作为参数
function print(fn) {
    let a = 200
    fn()
}
let a = 100
function fn() {
    console.log(a)
}
print(fn)

从上面代码中可得出 函数中自由变量在使用时 **函数定义的地方(不是执行的地方)**去寻找上级作用域

这种现象称为闭包

闭包示例

隐藏定义的数据 只提供API

js

function createCache() {
    let data = {}  // 闭包中的数据,被隐藏,不被外界访问
    return {
        set: function (key, val) {
            data[key] = val
        },
        get: function (key) {
            return data[key]
        }
    }
}
let c = createCache()
c.set('a', 100)
console.log( c.get('a') )

this

常见场景

  • 作为普通函数调用
  • 使用 call apply bind
  • 作为对象方法调用
  • 在 class 的方法中调用
  • 箭头函数

js

// this 场景题 - 1
function fn1() {
    console.log(this)
}
fn1() // 打印什么

fn1.call({ x: 100 }) // 打印什么

const fn2 = fn1.bind({ x: 200 })
fn2() // 打印什么

// this 场景题 - 2
const zhangsan = {
    name: '张三',
    sayHi() {
        console.log(this)
    },
    wait() {
        setTimeout(function() {
            console.log(this)
        })
    },
    waitAgain() {
        setTimeout(() => {
            console.log(this)
        })
    }
}
zhangsan.sayHi() // 打印什么
zhangsan.wait() // 打印什么
zhangsan.waitAgain() // 打印什么

// this 场景题 - 2
class People {
    constructor(name) {
        this.name = name
        this.age = 20
    }
    sayHi() {
        console.log(this)
    }
}
const zhangsan = new People('张三')
zhangsan.sayHi() // 打印什么

this的指向不用特意去死记硬背 抓准核心点:看调用对象

题目解答

this 的不同应用场景

  • 作为普通函数调用 -> 一般指向函数调用者
  • 使用 call apply bind -> 改变this指向
  • 作为对象方法调用 -> 一般指向调用对象
  • 在 class 的方法中调用 -> 一般指向实例
  • 箭头函数 -> 指向箭头函数定义作用域

创建 10 个<a>标签,点击的时候弹出来对应的序号

js

// 创建 10 个`<a>`标签,点击的时候弹出来对应的序号
let i, a
for (i = 0; i < 10; i++) {
    a = document.createElement('a')
    a.innerHTML = i + '<br>'
    a.addEventListener('click', function (e) {
        e.preventDefault()
        alert(i)
    })
    document.body.appendChild(a)
}

经典作用域问题,解决方案:

  1. 使用let定义i 形成局部作用域 限制自由变量的查找层级
  2. 通过闭包

实际开发中闭包的应用

参考之前的示例。再次强调闭包中自由变量的寻找方式!!!

js

// 隐藏数据,只提供 API
function createCache() {
    let data = {}  // 闭包中的数据,被隐藏,不被外界访问
    return {
        set: function (key, val) {
            data[key] = val
        },
        get: function (key) {
            return data[key]
        }
    }
}
let c = createCache()
c.set('a', 100)
console.log( c.get('a') )

手写 bind 函数

先回顾 bind 函数的应用

js

function fn1(a, b) {
    console.log('this', this)
    console.log(a, b)
}
const fn2 = fn1.bind({ x: 100 }, 10, 20) // bind 返回一个函数,并会绑定上 this
fn2()

手写 bind

js

Function.prototype.bind1 = function () {
    // 将参数解析为数组
    const args = Array.prototype.slice.call(arguments)

    // 获取 this(取出数组第一项,数组剩余的就是传递的参数)
    const t = args.shift()

    const self = this // 当前函数

    // 返回一个函数
    return function () {
        // 执行原函数,并返回结果
        return self.apply(t, args)
    }
}