0%

作用域和闭包

  • 函数声明和函数表达式是什么,又有什么区别呢?

我们来看如下的代码:

1
2
3
4
5
6
7
8
9
fn();
function fn () {
//声明
}

fn1();
var fn1 = function () {
//表达式
}

我们知道函数声明和变量都会进行提升,在函数声明之前调用函数不会报错,因为编译器已经知道这个是一个函数了,可以调用。

但是对于函数表达式来说,将函数表达式赋值给变量的时候,变量是得到提升了,但是赋值操作是在执行过程中进行的,所以这里在表达式执行之前,fn1变量其实是undefined的。调用就会报错。

  • 我们再来看一个例子:
    1
    2
    console.log(a);
    var a = 100;

如果你觉得输出的结果是undefined,那么恭喜你已经对提升有了较好的理解了。

a作为一个全局变量得到了提升,在a未赋值之前,a都被认为是undefined。

函数在执行之前就已经确定了this和arguments的值

  • 先了解一下执行上下文的概念:一段script或者函数都可以创建执行上下文,对于script来说,生成全局执行上下文,执行之前将变量定义和函数声明拿出来。

对于函数来说,会在函数执行之前将函数中的变量定义、函数声明,this和arguments拿出来。

  • this会在执行时才能确定值,定义时无法确认,比如下面的例子。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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

变量提升如何理解

  • 变量定义

  • 函数声明


this几种使用场景

  • 作为构造函数执行
1
2
3
4
function Foo(name) {
this.name = name
}
var f = new Foo('zhangsan);
  • 作为对象属性执行
1
2
3
4
5
6
7
var obj = {
name: 'A',
printName: function () {
console.log(this.name)
}
};
obj.printName();
  • 普通函数执行
1
2
3
4
function fn() {
console.log(this);
}
fn();
  • call,apply,bind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fn1(name) {
alert(name);
console.log(this);
}
fn1.call({x:100}, 'zhangsan')
fn1.apply({x:100},['zhangsan'])


var fn2 = function (name) {
alert(name);
console.log(this);
}.bind({y:200})

fn2('zhangsan');

注意bind方法只用于函数表达式


创建十个a标签,点击弹出相应序号

这是一种错误的写法

1
2
3
4
5
6
7
8
9
var 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);
})
}

我们点击的时候肯定是在代码执行完毕过后,那么我们给每个a标签绑定的点击事件的回调函数是alert(i),此时i为一个自由变量,就会向父作用域查找,而此时执行完毕for循环后,i的值为10,所以无论点击哪个a标签产生的结果都为10。

下面是正确的写法

1
2
3
4
5
6
7
8
9
10
11
12
var i;
for (i = 0; i < 10; i++) {
(function (i) {
var a =document.createElement('a');
a.innerHTML = i + '</br>';
a.addEventListener('click', function (i) {
alert (i);
});
document.body.appendChild(a);
})(i)
}


如何理解作用域

  • JS没有块级作用域
1
2
3
4
5
// 没有块级作用域
if (true) {
var name = 'zhangsan';
}
console.log(name); // zhangsan

在java或者c#中在大括号内定义的变量在外部是访问不到的,称之为块级作用域。
js中在块内和块外声明变量是一样的,所以不推荐在块内声明变量。

  • 只有函数和全局作用域(相对于低级语言)
1
2
3
4
5
6
7
8
// 函数和全局作用域
var a = 100;
function fn () {
var a = 200;
console.log('fn', a);
}
console.log('global', a);
fn();

函数作用域可以保证变量不被污染。

下面这段代码解释了作用域链

1
2
3
4
5
6
7
8
9
var a = 100;
function fn () {
var b = 200;
// 自由变量在当前函数作用域没有定义,会沿着作用域链向上查找,比如这里就向上查找到全局作用域
console.log(a);

console.log(b);
}
fn();
1
2
3
4
5
6
7
8
9
10
11
12
var a = 100;
function F1() {
var b = 200;
function F2() {
var c = 300;
console.log(a);
console.log(b);
console.log(c);
}
F2();
}
F1();

总结一下:执行环境定义了变量或者函数有权访问的其他数据。每个执行环境都有一个变量对象(variable object),环境中定义的所有变量和对象都保存在这个对象中,但是我们的代码不能够访问这个对象,解析器在处理数据时在后台使用它。

而全局执行环境是最外围的执行环境,根据ECMAScript宿主环境的不同,表示执行环境的对象不同,在浏览器中,全局执行环境对象被认为是window对象。

每个函数也有自己的执行环境,当执行流进入一个函数时,函数的环境就被推入到一个环境栈中。执行结束之后,栈就将其环境弹出,把控制权交给之前的执行环境。

当代码在一个环境中执行时,就会创建变量对象的一个作用域链。他的用途,就是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行代码所在环境的变量对象。如果是函数,则将其活动对象作为变量对象。活动对象一开始只包含arguments对象。作用域链的下一个变量对象就来自于包含环境,再下一个变量对象来自于下一个包含环境,这样一层层追溯到全局执行环境。


实际开发闭包的应用

首先来说明一下闭包的概念:闭包是指有权访问另一个函数作用域中的变量的函数。首先是函数,然后可以访问其他函数作用域中的变量。

闭包的两个场景

  • 函数作为返回值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function F1 () {
    var a = 100;

    return function () {
    console.log(a); // 自由变量去父级作用域查找,定义的父级作用域就是F1的函数作用域
    }
    }
    // f1得到一个函数
    // 函数的父级作用域是定义时候的作用域,而不是执行时候的父级作用域
    var f1 = F1();
    var a = 200;
    f1();// 100
  • 函数作为参数传递
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function F1 () {
    var a = 100;
    return function () {
    console.log(a);
    }
    }

    var f1 = F1();

    function F2 (fn) {
    var a = 200;
    fn();
    }

    F2(f1);

封装变量和收敛权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

表现为_list不能随意被外部修改。


2018 年 1月 5日