作用域与闭包

概念

闭包是指有权访问另一个函数作用域中变量的函数

创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包有两个常用的用途:

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。直观的说就是形成一个不销毁的栈环境。

闭包作用域

函数作为返回值

  • fn函数内定义了一个局部变量,并返回一个匿名函数结果,该匿名函数保留了对该变量的引用。
  • 在外层,我们使用f接收fn的返回值,得到的返回结果是一个函数,通过调用该函数,使得我们在fn函数的外部能够访问到函数内部的变量,同时调用之后该变量不会被回收。
  • 注意,内部的函数绑定的是它附近的环境
1
2
3
4
5
6
7
8
9
10
11
12
function fn() {
var cnt = 1; // 内部的函数绑定的是它附近的环境
return function() {
cnt++;
console.log(cnt)
}
}
var cnt = 2 // 与此行无关
var f = fn() // 以函数作为返回值 得到的是一个函数
f() // 2
f() // 3
f() // 4

函数作为参数

注意,将fn传入test函数中,fn内引用的cnt是它那个环境下的变量cnt,值为2,所以最后输出结果为3

1
2
3
4
5
6
7
8
9
10
11
12
function test(f) {
var cnt = 1;
f()
}

var cnt = 2
function fn() {
cnt ++;
console.log(cnt)
}

test(fn) // 3

作用域

全局作用域和函数作用域

(1)全局作用域

  • 最外层函数和最外层函数外面定义的变量拥有全局作用域
  • 所有未定义直接赋值的变量自动声明为全局作用域
  • 所有window对象的属性拥有全局作用域
  • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。

(2)函数作用域

  • 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
  • 作用域是分层的,内层作用域可以访问外层作用域,反之不行

块级作用域

  • 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)
  • let和const声明的变量不会有变量提升,也不可以重复声明
  • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。

作用域链

在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

this指向

this的指向是在使用时决定的,而不是定义时

调用方式 this指向
普通函数调用 window全局
定时器函数 window全局
立即执行函数 window全局
构造函数调用 实例对象
对象方法调用 该方法所属对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Person {
constructor(name, age) {
this.name = name
this.age = age
console.log('构造函数里的this指向:', this) // p
}

test() {
console.log('对象方法里的this指向:', this) // p
}

asyncTest() {
// setTimeout中的function实际上是在全局环境中执行的
setTimeout(function() {
console.log('定时器内普通函数的this指向:', this) // window
},0)

setTimeout(() => {
console.log('定时器内箭头函数的this指向:', this) // p
}, 0)
}
}

var p = new Person('yluy', 12)
p.test()
p.asyncTest()

function fn() {
console.log('普通函数内的this指向:', this)
}

fn() // window

call / apply / bind 函数

call函数

call[thisArg, args1, ..., args2]

  • 可以调用函数
  • 可以改变函数内部的this指向
  • 可以实现继承
  • 传入的参数数量不固定,第一个参数是代表函数体内的this指向,从第二个参数开始往后是传入函数内的参数

手写call函数

  • 处理传入的参数,第一个参数作为上下文对象(this),之后的参数传入需要调用的方法
  • 将函数作为上下文对象的一个属性
  • 使用上下文对象来调用这个方法,并保存返回的结果
  • 删除刚才新增的属性
  • 返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Function.prototype.myCall = function() {
const args = Array.prototype.slice.call(arguments)
const self = this
const t = args.shift()
t.test = self // 将函数作为上下文对象的一个属性
const res = t.test(...args) // 传入参数
delete t.test
return res
}

function test(a, b, c) {
console.log(a, b, c)
console.log("此时的this的指向:", this)
return "success"
}

const res = test.myCall({name: 'yluy'}, 10, 11, 12)
console.log('res:', res)

apply函数

call[thisArg, [args1, ..., args2]]

apply 接受两个参数

  • 第一个参数指定了函数体内 this 对象的指向,
  • 第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。

手写apply函数

  • 处理传入的参数,第一个参数作为上下文对象(this),之后的参数传入需要调用的方法
  • 将函数作为上下文对象的一个属性
  • 使用上下文对象来调用这个方法,并保存返回的结果
  • 删除刚才新增的属性
  • 返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.myApply = function() {
const self = this
const t = arguments[0] // 保存上下文对象
t.test = self // 将函数作为上下文对象的一个属性
const res = t.test(...arguments[1]) // 使用上下文对象来调用该方法
delete t.test
return res
}

function test(a, b, c) {
console.log(a, b, c)
console.log("此时的this的指向:", this)
return "success"
}

const res = test.myApply({name: 'yluy'}, [10, 11, 12])
console.log('res:', res)

bind函数

  • 改变this指向
  • 第一个参数是this的值,后面的参数是函数接收的参数的值
  • 改变this前后,原返回值不变

手写bind函数

var f2 = test.bind({name : 'yluy'}, 12, 3, 4)进行分析:

  • test.bind()中的bind在调用时,内部的this指针指向的是test函数
  • bind函数其实是Function原型对象的方法
  • 注意,f2拿到的返回值是一个函数

详细步骤

  • 获取当前函数的引用,获取其余传入参数值
  • 创建一个函数返回
  • 函数内部使用apply来绑定函数调用, 这个时候需要传入当前函数的this给apply调用,其余参数都传入指定的上下文对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function test(a, b, c) {
console.log(a, b, c)
console.log("此时的this的指向:", this)
return "success"
}

// var f1 = test(1, 2, 3)
// var f2 = test.bind({name : 'yluy'}, 12, 3, 4)

Function.prototype.mybind = function() {
const self = this // this指向原函数 也就是test
const args = Array.prototype.slice.call(arguments)
const thisValue = args.shift()

// bind返回一个新的函数
return function() {
return self.apply(thisValue, args)
}
}

var f3 = test.mybind({name : 'sonata'}, 12, 3, 4)
console.log(f3())