1 前言
这四点是在面试中经常会被问到的,这两天终于查了书也看了几篇博客,在此做整理。
这也提醒我们看书过后不代表掌握,任何学习过程都要经历懂-会-对-悟四个阶段,准备面试过程其实也是对知识点的梳理过程。
以下内容,欢迎指正。
2 闭包
参考
《JavaScript高级程序设计》
https://juejin.im/entry/57d60f7067f3560057e37e25
https://segmentfault.com/q/1010000006873055
2.1 什么是闭包?
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式是在一个函数内部创建另一个函数。
相关概念:
执行环境:定义了变量或函数有权访问的其他数据。全局执行环境是最外围的环境,每个函数也有自己的执行环境。
变量对象/作用域:每个执行环境都有一个与之关联的变量对象,环境中的所有变量和函数都保存在这个对象中。
作用域链:代码在一个环境中执行时,会创建变量对象的一个作用域链。如果环境是函数则将其活动对象作为变量对象。活动对象在最开始只包含一个变量,即arguments对象,作用域链中下一个变量对象来自包含环境,以此类推直到全局执行环境。全局执行环境的变量对象始终是作用域链中最后一个对象。
2.2 闭包注意事项
(1) 在函数中访问变量时,会从作用域链中搜索具有相应名字的变量。一般情况下,函数执行完毕后局部活动对象就会销毁,内存中仅仅保存全局作用域。但是闭包情况有所不同。直到匿名函数被销毁,包含函数的活动对象才会被销毁。
(2) 闭包只能取得包含函数中任何变量的最后一个值。因为闭包保存的是整个变量对象,而不是某个特殊的变量。
function create() {
var arr = new Array()
for (var i = 0; i < 10; i++) {
arr[i] = function() {
return i
}
}
console.log(arr[5]())
}
create() //10
稍作修改
function create() {
var arr = new Array()
for (var i = 0; i < 10; i++) {
arr[i] = (function(num) {
return num
})(i)
}
console.log(arr[5])
}
create() //5
或者
function create() {
var arr = new Array()
for (var i = 0; i < 10; i++) {
arr[i] = (function(num) {
return function() {
return num
}
})(i)
}
console.log(arr[5]())
}
create() //5
第二种第三种本质上是一样的。注意,如果把var改为let也能达到同样的效果。对于之前的create()函数作用域取得i的值是for循环执行完后的值10,但使用let后i与for循环之外的区域无关,此时闭包访问到的最后一个值则是进入当前循环的i值。
2.3 闭包应用
(1) 匿名函数创建闭包,模仿块级作用域,既能使用自己的变量又不必担心搞乱全局作用域。
(function(){
})();
在JavaScript中,多次重复声明一个变量并不会改变它的值。它会对后续声明视而不见(会执行初始化)。比如
function create() {
for (var i = 0; i < 10; i++) {
console.log(i)
}
var i
console.log(i)
}
create() //输出0-10
为了避免这种情况,就可以采用匿名函数。
function create() {
;(function() {
for (var i = 0; i < 10; i++) {
console.log(i)
}
})()
var i
console.log(i)
}
create() //输出0-9和undefined 去掉var i则会报错ReferenceError 从而保证了i在外部无法访问
(2) 创建特权方法:用于访问私有变量的公有方法。
1 构造函数模式-构造函数中使用
function MyObject(){
var privateNum=10;
this.publicMethod=function(){
privateNum++;
return privateNum;
}
}
2 原型模式-私有作用域中使用
(function(){
var privateNum=10;
MyObject=function(){
};//构造函数
MyObject.prototype.publicMethod=function(){
privateNum++;
return privateNum;
}
})();
此处声明MyObject没有使用var,使之成为了一个全局变量,能够在私有作用于之外被访问到。
3 模块模式
适用于创建单例对某些数据进行初始化同时需要公开一些可访问这些私有数据的方法。
var singleObj=function(){
var privateNum=10;
return {
publicMethod: function(){
return privateNum;
}
}
}
4 增强的模块模式
适用于单例必须是某些类型实例的情况。
var singleObj=function(){
var privateNum=10;
var obj=new CustomType();
obj.publicMethod=function(){
return privateNum;
}
return obj;
}
前两种是自定义类型的特权方法,后两种是单例的特权方法。
3 this对象
this对象是在运行时基于函数的执行环境绑定的。全局函数中this等于window,当函数作为某个对象的方法调用时,this等于那个对象。匿名函数的执行环境具有全局性,因此其this对象通常指向window。
最重要的是基于运行时绑定,this处于函数作用域中,但值是window,与作用域链搜索变量值是有区别的。很长一段时间我并未意识到这一点,因此总是混淆。实际上,记住并理解上面这一段话就足够应对this的问题了。
例1
var name = 'the window'
var object = {
name: 'the object',
getName: function() {
return function() {
return this.name
}
}
}
console.log(object.getName()())
//"the window"
解答:运行时在全局环境中,对于匿名函数而言,这与直接在外部使用没有区别。
var name = 'the window'
var getName = function() {
return this.name
}
console.log(getName())
////"the window"
例2
var name = 'the window'
var object = {
name: 'the object',
getName: function() {
var that = this
return function() {
return that.name
}
}
}
console.log(object.getName()())
//"the object"
解答:每个函数在被调用时都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。因此要把需要的this保存在闭包可以访问的变量里。在例2中,getName方法作为object的方法调用,所以其中this等于object,而this又传递给了that,所以that.name是"the object"。
例3 箭头函数
箭头函数体内的this对象就是定义时所在的对象,而不是使用时所在的对象。
其实这句话比较容易混淆,不过如果记住,箭头函数没有自己的this,内部的this就是外层代码块的this,或者说第一个非箭头函数中的this,那么应该足够了。
(function() {
var showNumber = () => {
console.log(this === num); // true
console.log(this); // [1, 2, 3]
}
console.log(this === num); // true
showNumber(); // true && [1, 2, 3]
showNumber.call([1, 2]); // true && [1, 2, 3]
showNumber.apply([1, 2]); // true && [1, 2, 3]
showNumber.bind([1, 2])(); // true && [1, 2, 3]
}).call(num);
解答:showNumber中箭头函数绑定的this是匿名函数function中的,牢记这一点。call传入num数组时,其实就是更改function中this的值为num,所以第三处是没疑问的。而第一次生成箭头函数时,this值就是function中的num,所以第一处第二处分别是true和num值。后面的四处运行包括call、apply、bind都是无法修改第一次绑定的this值的,因为箭头函数this固定,所以结果和第一次一样。
例4 箭头函数 setTimeout
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
先对比稍作修改后的运行结果
function foo() {
setTimeout(function() {
console.log(this)
console.log('id:', this.id)
}, 100)
}
var id = 21
foo.call({ id: 42 })
// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
// id: 21
解答:第一段代码绑定的是foo中的this,第二段this依然是window,与foo中的this无关。
对于第一段中箭头函数定义时机有疑问的可以参考这个链接下阮一峰作者的回答 https://github.com/ruanyf/es6tutorial/issues/150
请问,上面代码的{id: 42},到底是箭头函数定义时所在的对象,还是运行时所在的对象?
你认为,答案是后者。这是不对的。
因为,这个例子中,箭头函数位于foo函数内部。只有foo函数运行后,它才会按照定义生成,所以foo运行时所在的对象,恰好是箭头函数定义时所在的对象。
4 setTimeout
参考这两篇博客对于事件循环机制和该函数的介绍
https://juejin.im/post/5aa4c47af265da239866e236#comment
https://www.jianshu.com/p/3e482748369d?from=groupmessage