第4章 JS基础-作用域和闭包【三座大山之二,不会闭包,基本不会通过】

返回章节目录

目录

1.作用域和自由变量

作用域

自由变量

2.闭包

3.this

4.练习题

1.手写bind函数

2.做一个简单的cache工具

3.创建10个标签,点击的时候弹出对应的序号


1.作用域和自由变量

作用域

一共4个红框,分别代表了a,a1,a2,a3这些变量的合法使用范围

作用域分为

全局作用域(不讲)

函数作用域(不讲)

块作用域(ES6新增)

块作用域这里let和const都一样,超出范围报错

自由变量

一个变量在当前作用域没有定义,但被使用了,则向上级作用域一层一层依次寻找,直至找到为止,如果到全局作用域都没找到,则报错 xx is not defined。可以这样理解,凡是跨了自己的作用域的变量都叫自由变量。

2.闭包

闭包是作用域应用的特殊情况,有两种表现:

1.函数作为参数被传递

2.函数作为返回值被传递

简单理解:当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包

function create() {
    const a = 100
    return function () {
        console.log(a)
    }
}

const fn = create()
const a = 200
fn() // 100

这段代码打印100,这个so easy

function print(fn) {
    const a = 200
    fn()
}
const a = 100
function fn() {
    console.log(a)
}
print(fn) // 打印多少呢

这段代码打印100

如果这2个例子很轻松看出结果说明闭掌握的比较好

说一下第二个例子为什么不是200

所有的自由变量的查找,是在函数定义的地方向上级作用域查找,不是在执行的地方!

所以这里函数定义的地方往上查找就是100

进一步说明是在函数定义的地方向上级作用域查找,找不到就直接报错!如果在找不到的情况接着在执行的地方查找就会打印200,但是这里不打印,说明不会在执行的地方查找。

3.this

this的应用场景一般如下

1.在普通函数使用

2.使用call、apply、bind

3.在对象方法中被调用

4.在class方法中调用

5.在箭头函数中使用

this的取值是在函数执行的时候确定的,不是在函数定义的时候确定的,这个规则适用于上面所有场景

来看看例子

这里直接fn1()打印的this是window

调用call之后打印是this是这个对象{ x: 100}

调用bind之后不会直接执行,返回另外一个函数,执行这个函数,this指向bind的对象{ x : 200 }

sayHi()里面的this就是当前对象,这里setTimeout里面的函数和普通函数一样,this就是window

箭头函数中的this是上级作用域的this的值,嵌套setTimeout也是一样,因为最外层setTimeout的this是上级作用域的this,所以最内层的setTimeout中的this还是和外层的一样,即上级作用域的this

class里面的this就是指的当前实例对象

4.练习题

1.手写bind函数

如果要实现一下代码的功能,将其中的bind重写一下该如何实现呢?先来看看api的实现结果

function fn1(a, b, c) {
    console.log('this', this)
    console.log(a, b, c)
    return 'this is fn1'
}

const fn2 = fn1.bind({x: 100}, 10, 20, 30)
const res = fn2()
console.log(res)

运行结果: 

手写bind函数如下:

// 老师模拟 bind的代码,我觉得需要一点优化
/*Function.prototype.bind1 = function () {
    // 将参数拆解为数组
    const args = Array.prototype.slice.call(arguments)

    // 获取 this(数组第一项)
    const t = args.shift() // 获取并删除第一个对象

    // fn1.bind(...) 中的 fn1
    const self = this // 谁调用函数this就是谁

    // 返回一个函数
    return function () {
        return self.apply(t, args)
    }
}*/
// 我自己模拟 bind函数
Function.prototype.bind1 = function () {
    // 获取 this(数组第一项)
    const t = arguments[0]

    // fn1.bind(...) 中的 fn1
    const self = this // 谁调用函数this就是谁

    // 将参数拆解为数组
    const args = Array.prototype.slice.call(arguments, 1)


    // 返回一个函数
    return function () {
        return self.apply(t, args)
    }
}

function fn1(a, b, c) {
    console.log('this', this)
    console.log(a, b, c)
    return 'this is fn1'
}

const fn2 = fn1.bind1({x: 100}, 10, 20, 30)
const res = fn2()
console.log(res)

当然运行结果和原生bind是一样的。为什么我说需要优化一下老师的代码?

老师代码步骤先获取新数组(arguments转化为数组),然后再去删除第一个元素(这个过程很耗时,因为除了第一个元素后面依次往前移动,最后长度减少1)

我的bind就是先获取第1个参数,方便后面apply绑定用,然后再获取新数组(arguments从下标1开始往后),这样就少去后面依次往前移动的步骤。

这里涉及到了很多api,这个需要积累熟悉,可以自寻官网文档

来看看官网文档Polyfill写法其实和上面差不多,兼容性更好,这段代码可以使你的 bind() 在没有内置实现支持的环境中也可以部分地使用bind

// Does not work with `new funcA.bind(thisArg, args)`
if (!Function.prototype.bind) (function(){
  var slice = Array.prototype.slice;
  Function.prototype.bind = function() {
    var thatFunc = this, thatArg = arguments[0]; // 取第一个参数,目标是this指向的第一个参数
    var args = slice.call(arguments, 1); // 从下标1开始将arguments类数组转换成数组
    if (typeof thatFunc !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - ' +
             'what is trying to be bound is not callable');
    }
    return function(){
      var funcArgs = args.concat(slice.call(arguments)) //这个arguments是return的匿名函数的,和上面的arguments没有关系,不要混淆
      return thatFunc.apply(thatArg, funcArgs);// 最后返回的函数中return this.apply(第一个参数,除了第一个参数之后的参数(可能有bind返回后再次传参的))
    };
  };
})();

2.做一个简单的cache工具

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

const c = createCache()
c.set('a', 100)
console.log( c.get('a') )

如果不调用set方法,就不能设置键值对,如果不调用get方法,就不能存储键值对

比如想要设置data.b = 101会显示data is not defined,无法访问data

3.创建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)
}

for循环很快结束了,i为10,回调函数里面引用了外部的i,形成了闭包,回调函数什么时候执行呢?什么时候点击就什么时候执行。等到点击的时候for循环早就执行完了,i为10,所以点击每个a的时候都是弹出10

let a
for (let 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)
}

还是块作用域的问题,因为i现在不是全局的,是在for循环的块里面的,回调函数在往上找i变量的时候就会在块里面找到,每次循环都是一个块作用域,自然就对应i的值。

关注、留言,我们一起学习。

===============Talk is cheap, show me the code================

发布了224 篇原创文章 · 获赞 983 · 访问量 91万+

猜你喜欢

转载自blog.csdn.net/qq_34115899/article/details/104105539
今日推荐