第三部分 作用域和闭包
对应知识点
1、执行上下文
变量提升
(1)JavaScript 中,函数及变量的声明都将被提升到函数的最顶部。
(2)JavaScript 中,变量可以在使用后声明,也就是变量可以先使用再声明。
(3)声明提升:函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部。
console.log(a) // undefined
var a = 10;
fn('zhansan') // 'zhansan' 20
function fn(name) {
age = 20;
console.log(name, age)
var age
}
**范围:**一段 script 或者一个函数
全局: (全局上下文) 变量声明定义、函数声明
函数: (函数上下文) 变量声明定义、函数声明、this、arguments(函数参数集合)
注意:“函数声明” 和 “函数表达式” 的区别
2、this
(1)this 要在执行时才能确认值,定义时无法确认值
var a = {
name: 'A',
fn: function () {
console.log(this.name)
}
}
a.fn() // this 指向 a 这个对象
a.fn.call({ name: 'B' }) // this 指向 {name:'B'}
var fn1 = a.fn;
fn1() // this 指向 window
(2)作为构造函数执行
this 指向 new 出来的对象 f
// 作为构造函数执行
function Foo(name, age) {
// this = {}
this.age = age
this.name = name;
// return this
console.log(this, '构造函数this') // this 指向 new 出来的对象 f
}
var f = new Foo('zhangsan', 20)
(3)作为对象属性执行
this 指向该属性所属的对象 obj
// 作为对象属性执行
var obj = {
name: 'lisi',
printName: function () {
console.log(this.name)
console.log(this) // this 指向该属性所属的对象 obj
}
}
obj.printName()
(4)作为普通函数执行
this 指向 window对象
// 作为普通函数执行
function fn() {
console.log(this) // this 指向 window对象
}
fn()
(5)call() apply() bind()
**共同点:**三者都可以改变函数执行时的上下文,即改变 this 的指向,三者的第一个参数都是修改后的 this
区别:
1、传参方式不同
—> call 第一个参数是对象,没有则指向 window ,从第二个参数起就是逐一传参(参数列表),这些参数与函数参数是对应的。
函数名.call(修改的 this ,参数1,参数2…)
—> apply 只接受两个参数,第一个参数是对象,没有则指向 window , 第二个参数是数组或者类数组(集合),数组或类数组的
元素对应函数参数。函数名.apply(修改的 this ,数组或者类数组)
—>bind 第一个参数是对象,没有则指向 window,从第二个参数起就是逐一传参(参数列表),这个参数可以分多次传入。
函数名.bind (修改的 this ,参数1,参数2…)
2、执行机制不同
—> 改变 this 指向后,call 和 apply 会立即调用,bind不会立即调用,而是返回一个永久改变 this 指向的函数
call()
使用举例
// call
function thisCall(...args) {
console.log(this, args)
}
var dog = {
name: 'laifu'
}
// this 指向改变为 dog 对象,传入一个参数列表
thisCall.call(dog, 2, '香肠');
thisCall(3, 4) // this 指向 window
实现call
考虑 call 的功能,存在两个关键点
—> 不传入第一个参数,默认为 window
—> 改变了 this 指向,让新的对象(改变后 this 指向的对象)可以执行该函数,
—> **思路:**给新对象添加一个函数,执行完之后删除
// 实现 call
Function.prototype.myCall = function (context){
var context = context || window
// 给 context 添加一个属性,
// getValue.call(a,'yck','24') => a.fn = getValue
context.fn = this
// 将 context 后面的参数取出来
var args = [...arguments].slice(1)
// getValue.call(a,'yck','24') => a.fn('yck','24')
var result = context.fn(...args)
// 删除 fn
delete context.fn
return result
}
apply()
使用举例
// apply 只接受两个参数 第一个参数为指向对象,第二个参数为数组或者类数组
function thisApply(...args) {
console.log(this, args);
}
var person = {
name: 'zhangsan'
}
// this 指向改变为 person 对象 , 传入的第二个参数必须是一个数组或者类数组
thisApply.apply(person, [1, 2]);
thisApply(1, 2); // this 指向 window
实现apply
// 实现 apply
Function.prototype.myApply = function (context) {
var context = context || window
context.fn = this
var result
// 需要判断是否存储第二个参数
// 如果存在 就将第二个参数展开
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
bind()
使用举例
// bind
function thisBind(age) {
console.log(this, age, 89898); // ƒ thisBind(age) { console.log(this, age, 89898); }
}
var cat = {
name: 'mimi'
}
// 改变了 thisBind 中的this,this 指向改变为 cat 对象,thisBind 并不立即执行,
// 并且返回了返回一个永久改变 this 指向的函数
thisBind.bind(cat, 3)
thisBind(6,7)
实现bind
需要考虑以下三点
—> bind 返回一个函数
—> 参数可以多次传入
—> 当 bind 返回的函数作为构造函数的时候,bind 执行时,指定的 this 值会失效,但传入的参数依然生效
// 实现 bind
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回一个函数,我们可以 new F(), 所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context,args.concat(...arguments))
}
}
3、作用域
(1)没有块级作用域
// (1)没有块级作用域
if (true) {
var name = 'zhangsan' // 在这里声明定义
}
console.log(name) // 在 if 块外面也可以获取 即没块级作用域
// 良好的代码习惯 在哪里声明定义就在哪里用 不推荐这样使用
(2)只有函数(局部)作用域和全局作用域
// (2) 函数(局部)作用域和全局作用域
var a = 100
function fn() {
var a = 200
console.log('fn', a) // 函数(局部)作用域
}
console.log('global', a) // 全局作用域
fn()
4、作用域链
**解释:**某个作用域中的“自由变量”向父级作用域寻找自身声明定义的过程就形成作用域链。
(1)当前作用域没有定义的变量,即 “自由变量”。
(2)变量或函数在哪里声明定义就往哪里去找其父级作用域。
// 4、作用域链
var c = 300
function fn1(){
var b = 500
// 当前作用域没有定义的变量,即 “自由变量”
function fn2(){
var c = 700
console.log(a) // a在全局作用域定义的 即a在该函数中是自由变量
console.log(b) // b 在fn2 的父级 fn1 函数作用域定义 b 也是自由变量
console.log(c)
}
fn2()
}
5、闭包
解释:闭包指的是那些引用了另外一个函数作用域中变量的函数, 通常是在嵌套函数中实现的。
(1)函数作为返回值
// 函数作为返回值
function F1() {
var a = 100
// 返回一个函数 (函数作为返回值)
return function () {
console.log(a) // 100
}
}
// f1 得到一个函数 赋值给f1
var f1 = F1()
var a = 200
f1()
(2)函数作为参数传递
// 函数作为参数来传递
function F1() {
var a = 100
// 返回一个函数 (函数作为返回值)
return function () {
console.log(a) // 100
}
}
// f1 得到一个函数 赋值给f1
var f1 = F1()
function F2(fn) {
var a = 200
fn()
}
F2(f1) // a 100 函数作为参数来传递
对应面试题
1、说一下对变量提升的理解
(对应知识点------执行上下文)
围绕以下方面作答理解
(1)变量定义
(2)函数声明(注意函数声明和函数表达式的区别)
变量提升
JavaScript 中,函数及变量的声明都将被提升到函数的最顶部。
JavaScript 中,变量可以在使用后声明,也就是变量可以先使用再声明。
声明提升:函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部。
2、说明 this 几种不同的使用场景
(1)this 要在执行时才能确认值,定义时无法确认值
(2)作为构造函数执行
(3)作为对象属性执行
(4)作为普通函数执行
(5)call apply bind
3、创建10个 a 标签,点击的时候弹出来对应的序号
作用域问题
实现效果
var i
for (i = 0; i < 10; i++) {
(
function (i) {
var a = document.createElement('a')
a.innerHTML = i + '<br>'
a.addEventListener('click', function (e) {
e.preventDefault() // 取消事件的默认动作
alert(i)
})
document.body.appendChild(a) // appendChild() 方法可向节点的子节点列表的末尾添加新的子节点
}
)(i)
}
4、如何理解作用域
(1)没有块级作用域
(2)只有函数(局部)作用域和全局作用域
(3)自由变量
(4)作用域链,即自由变量的查找
(5)闭包的两个场景
—> 函数作为返回值
—> 函数作为参数传递
5、实际开发中闭包的应用
闭包实际应用中主要用于封装变量,收敛权限
// 实际中闭包的应用
// 闭包实际应用中主要用于封装变量,收敛权限
function isFirstLoad() {
var _list = []
return function (id) {
if (_list, indexOf(id) >= 0) {
return false
} else {
_list.push(id)
return true
}
}
}
// 使用
var firstLoad = isFirstLoad()
firstLoad(10) // true
firstLoad(10) // false
firstLoad(20) // true