目录
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================