作用域与闭包

一、作用域与自由变量

作用域是某个变量的合法使用范围

作用域包括

  • 全局作用域
  • 函数作用域
  • 块级作用域(ES6新增)

自由变量

  1. 一个变量在当前作用域使用了,但是没有定义
  2. 会向上级作用域一层一层寻找,直到找到为止
  3. 如果到了全局作用域都没有找到,则会报错

二、闭包

闭包就是能够读取其他函数内部变量的函数。

闭包是作用域应用的特殊情况,包括以下两种情况:

  • 函数作为参数被传递
1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数作为参数被传递
function print(fn) {
let a = 200;
fn();
}

let a = 100;
function fn() {
let a = 300;
console.log(a)
}

print(fn); // 300
  • 函数作为返回值
1
2
3
4
5
6
7
8
9
10
11
// 函数作为返回值被传递
function create() {
let a = 100;
return function () {
console.log(a)
}
}

let fn = create();
let a = 200;
fn(); // 100

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

三、闭包的应用

隐藏数据,让这些变量的值始终保持在内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 闭包隐藏数据,只提供API
function cacheControl() {
let data = {}
return {
set: function (key, value) {
data[key] = value
},
get: function (key) {
return data[key]
}
}
}

const cache = cacheControl();
cache.set('a', 100)
console.log(cache.get('a')); // 100

可以读取函数内部的变量

四、this的指向

this是在执行上下文创建时确定的一个在执行过程中不可更改的变量。

在日常应用最多的还是在函数中用this,函数的调用方式有4种:

在全局环境或是普通函数中直接调用

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
33
34
35
36
// 全局环境中直接调用
function f1() {
console.log(this)
}

f1(); // window

// 普通函数中调用
// 严格模式
let a = 1;
let obj = {
a: 2,
b: function () {
function fun() {
return this.a
}

console.log(fun());
}
}
obj.b(); // undefined


// 非严格模式
var a = 1;
var obj = {
a: 2,
b: function () {
function fun() {
return this.a
}

console.log(fun());
}
}
obj.b(); // 1

当函数独立调用的时候,在严格模式下它的this指向undefined,在非严格模式下,当this指向undefined的时候,自动指向全局对象(浏览器中就是window)。

作为对象的方法

1
2
3
4
5
6
7
8
9
// 作为对象的方法
let a = 1;
let obj = {
a: 2,
b: function() {
return this.a;
}
}
console.log(obj.b()) // 2

当作为对象的一个方法调用时,这时候this指向调用它的对象。即obj

如果b方法不作为对象方法调用,如下:

1
2
3
4
5
6
7
8
9
let a = 1;
let obj = {
a: 2,
b: function() {
return this.a;
}
}
let t = obj.b;
console.log(t()); // undefined

此时的t()是作为一个函数独立调用的。在严格模式下,是指向undefined的。

使用apply和call

1
2
3
4
5
6
7
8
function fun() {
return this.a;
}
fun();//1
//严格模式
fun.call(undefined)
//非严格模式
fun.call(window)

作为构造函数

所谓构造函数就是用来new对象的函数,像FunctionObjectArrayDate等都是全局定义的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Fun() {
this.name = 'le';
this.age = 21;
this.sex = 'man';
this.run = function () {
return this.name + '正在跑步';
}
}
Fun.prototype = {
contructor: Fun,
say: function () {
return this.name + '正在说话';
}
}
let f = new Fun();
f.run();//Damonare正在跑步
f.say();//Damonare正在说话

如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象

new运算符的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
function objectFactory() {

var obj = new Object(),

Constructor = [].shift.call(arguments);

obj.__proto__ = Constructor.prototype;

var ret = Constructor.apply(obj, arguments);

return typeof ret === 'object' ? ret : obj;

};
  1. 用new Object() 的方式新建了一个对象 obj
  2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
  3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
  4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
  5. 返回 obj

箭头函数

1
2
3
4
5
6
7
let a = 1;
let obj = {
a: 2
};
var fun = () => console.log(this.a);
fun(); // 1
fun.call(obj) // 1

箭头函数是一个不可以用call和apply改变this的典型。

箭头函数会捕获其所在上下文的 this 值,作为自己的 this,也就是说箭头函数的this在词法层面就完成了绑定。apply,call方法只是传入参数,却改不了this。

1
2
3
4
5
6
7
8
9
10
11
let a = 1;
let obj = {
a: 2
};
function fun() {
let a = 3;
let f = () => console.log(this.a);
f();
};
fun(); // undefined(严格模式undefined,非严格模式window)
fun.call(obj); // 2

如上,fun直接调用,fun的上下文中的this值为window(严格模式undefined,非严格模式window)。fun的上下文就是此箭头函数所在的上下文,因此此时f的this为fun的this也就是window。当fun.call(obj)再次调用的时候,新的上下文创建,fun此时的this为obj,也就是箭头函数的this值。

1
2
3
4
5
6
7
8
function Fun() {
this.name = 'le';
}
Fun.prototype.say = () => {
console.log(this);
}
let f = new Fun();
f.say(); // window

此时的箭头函数所在的上下文是__proto__所在的上下文也就是Object函数的上下文,而Object的this值就是全局对象。

1
2
3
4
5
6
7
8
function Fun() {
this.name = 'le';
this.say = () => {
console.log(this);
}
}
let f = new Fun();
f.say(); // Fun的实例对象

如上,this.say所在的上下文,此时箭头函数所在的上下文就变成了Fun的上下文环境,而因为上面说过当函数作为构造函数调用的时候(也就是new的作用)上下文环境的this指向实例对象。

五、手写一个bind函数

Function.prototype.bind()

bind()方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

1
2
3
4
5
6
7
8
9
10
function fn(a, b, c) {
let d = 10;
console.log('this', this); // {d: 100}
console.log('this.d', this.d); // 100
console.log('a,b,c', a, b, c); // 10, 20, 30
return 'this is fn';
}

const fn2 = fn.bind({d: 100}, 10, 20 ,30);
console.log(fn2()); // 'this is fn'

所以bind方法的三个要求:

  1. 返回一个函数
  2. 新函数this的指向bind方法的第一个参数
  3. 其他的参数作为新函数的参数供其使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Function.prototype.bind1 = function () {
// 获取传入的所有参数,转换为数组
const arr = Array.from(arguments)
// 取得第一项this
const t = arr.shift()
// fn.bind(...)中的fn
const self = this;
// 返回一个函数
return function () {
return self.apply(t, arr)
}
}

function fn(a, b, c) {
let d = 10;
console.log('this', this); // {d: 100}
console.log('this.d', this.d); // 100
console.log('a,b,c', a, b, c); // 10, 20, 30
return 'this is fn';
}

const fn2 = fn.bind1({d: 100}, 10, 20 ,30);
console.log(fn2()); // 'this is fn'
Author: YangLeLe
Link: http://younglele.cn/scope-and-closure/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.