前端面试的一些知识点(JavaScript) - this,作用域,闭包

this

###闭包:什么是闭包?

征服 JavaScript 面试:什么是闭包?| Eric Elliott

你不仅仅应该了解闭包的机制,更应该了解闭包为什么很重要,以及能够很容易地回答出闭包的几种可能的应用场景。

JavaScript 里的函数能访问它们的:
1,thisarguments
2,局部变量函数
3,外部函数的变量函数
4,全部变量(包括DOM)

闭包的产生

函数被创建时其实就产生了闭包,通常我们将一个函数定义在另一个函数内部,称这个内部函数为闭包

闭包的特点

JavaScript 采用了词法作用域,当闭包被传递到所在的词法作用域以外执行,它都会持有对原始定义作用域的引用,即使外部函数已经执行完毕,仍可通过闭包访问到外部函数作用域中的变量。

闭包的使用

1
2
3
4
5
6
7
// 不用 return 的闭包
function closureExample(objID, text, timedelay) {
setTimeout(function() { // 全局方法 window.setTimeout 持有了闭包。
document.getElementById(objID).innerHTML = text;
}, timedelay);
}
closureExample(‘myDiv’, ‘Closure is created’, 500);

例子一:闭包中的局部变量是引用而非拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function foo(x) {
var tmp = 3;
var func = function(y) {
alert(x + y + tmp);
}
tmp++;
return func;
}

var bar = foo(2);// x=2, y=10, tmp=4
bar(10); // 16

function say667() {
// Local variable that ends up within closure
var num = 666;
var sayAlert = function() { alert(num); }
num++;
return sayAlert;
}

var sayAlert = say667();
sayAlert();

例子二:多个闭包绑定同一个外部函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var gAlertNumber, gIncreaseNumber, gSetNumber;

function setupSomeGlobals() {
// Local variable that ends up within closure
var num = 666;
// Store some references to functions as global variables
gAlertNumber = function() { alert(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
}

setupSomeGlobals(); // 为三个全局变量赋值
gAlertNumber(); //666
gIncreaseNumber();
gAlertNumber(); // 667
gSetNumber(12);//
gAlertNumber();//12

例子三:当在一个循环中赋值函数时,这些函数将绑定同样的闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + list[i];
console.log(item);
result.push( function() {alert(item + ' ' + list[i])} );
}
return result;
}

function testList() {
var fnlist = buildList([1,2,3]);
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}

testList();
解读:

我的理解,循环中定义的 itemi 实际上在外部函数 buildList 作用域下,不是循环体中的块级作用域。

buildList 执行完毕后,最后一次循环使得 item 的值为 item3,跳出循环后 i 的值为 4,而 list[4] 的值为 undefined

修正:

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 循环体中使用了 let 定义块级变量 i 和 item。
function buildList(list) {
var result = [];
for (let i = 0; i < list.length; i++) {
let item = 'item' + list[i];
result.push( function() {alert(item + ' ' + list[i])} );
}
return result;
}

function testList() {
var fnlist = buildList([1,2,3]);
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}

testList();

方法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过IIFE传参的方式,把外部函数的变量闭包
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + list[i];
result.push(function(a,b) {
return function () {
alert(a + ' ' + b)
}
}(item, list[i]));
}
return result;
}

function testList() {
var fnlist = buildList([1,2,3]);
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}

testList();

闭包的应用场景

初级应用场景:事件处理回调函数中也常常会用到它。

高级应用场景:此外还有偏函数应用(partial applications)柯里化(currying),以及其他函数式编程模式。

如果你不能回答这个问题,你只是个初级开发者。不管你实际上已经干这个多久了。

闭包的应用场景(一):对象中数据的私有化(返回对象)

当你使用闭包来实现数据私有化时,被封装的变量只能在闭包容器函数作用域中使用。

只有定义在闭包作用域中的方法才可以访问这些数据。

闭包的应用场景(二):创建有状态的函数(返回函数)

闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性
变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。
不过,变量的生命周期是可以很长,在一次函数调用期间所创建所生成的值在下次函数调用时仍然存在。
正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中

1
2
3
4
5
6
7
// Return a function that approximates the derivative of f
// using an interval of dx, which should be appropriately small.
function derivative(f, dx) {
return function (x) {
return (f(x + dx) - f(x)) / dx;
};
}

debounce 防抖函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 将会包装事件的 debounce 函数
function debounce(fn, delay) {
// 维护一个 timer
let timer = null;
// 能访问 timer 的闭包
return function() {
// 通过 ‘this’ 和 ‘arguments’ 获取函数的作用域和变量
let context = this;
let args = arguments;
// 如果事件被调用,清除 timer 然后重新设置 timer
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
}
// 当用户滚动时被调用的函数
function foo() {
console.log('You are scrolling!');
}
// 在 debounce 中包装我们的函数,过 2 秒触发一次
window.addEventListener('scroll', debounce(foo, 2000));

throttle 节流函数