终于到做用域和闭包这一块了,这一块应该是JavaScript
语言里面最难以学习和理解的了.javascript
也许在平常编码中会常常接触到做用域和闭包,可是对于其原理和产生一系列的问题得不到一个深度的了解.做为一个[合格]的前端工程师,这一块的知识是必定要夯实的.前端
我在整理这一块的答案时,也从新理解了一遍做用域和闭包的知识,感受对于本身来讲,这又是一个提高,但愿这篇文章的答案可以和你们共勉.java
原文地址: 一名【合格】前端工程师的自检清单npm
词法做用域: 词法做用域(也就是静态做用域)就是定义在词法阶段的做用域,是由写代码时将变量和块做用域写在哪里来决定的,所以当词法分析器处理代码时会保持做用域不变;不管函数在哪里被调用,也不管它如何被调用,它的词法做用域都只由函数被声明时所处的位置决定.编程
动态做用域: 动态做用域并不关心函数和做用域是如何声明以及在任何处声明的,只关心它们从何处调用.换句话说,做用域链是基于调用栈的,而不是代码中的做用域嵌套.设计模式
JavaScript
采用词法做用域.promise
举个例子:浏览器
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 结果是 ???
复制代码
假设JavaScript
采用静态做用域,让咱们分析下执行过程:前端工程师
执行 foo
函数,先从 foo
函数内部查找是否有局部变量 value
,若是没有,就根据书写的位置,查找上面一层的代码,也就是 value
等于 1,因此结果会打印 1.闭包
假设JavaScript
采用动态做用域,让咱们分析下执行过程:
执行 foo
函数,依然是从 foo
函数内部查找是否有局部变量 value
.若是没有,就从调用函数的做用域,也就是 bar
函数内部查找 value
变量,因此结果会打印2.
前面咱们已经说了,JavaScript
采用的是静态做用域,因此这个例子的结果是1.
JavaScript
的做用域和做用域链让咱们用一段简单的代码来理解一下:
function fn() {
var innerVar = "内部变量";
}
fn();//要先执行这个函数,不然根本不知道里面是啥
console.log(innerVar); // Uncaught ReferenceError: innerVar is not defined
复制代码
从上面的例子能够体会到做用域的概念,变量 innerVar
在全局做用域没有声明,因此在全局做用域下取值会报错.
ES6
以前JavaScript
没有块级做用域,只有全局做用域和函数做用域,能够经过let
和const
来实现.
首先认识一下什么叫作自由变量 .以下代码中,console.log(a)
要获得a
变量,可是在当前的做用域中没有定义 a
(可对比一下b
).当前做用域没有定义的变量,这成为自由变量.自由变量的值如何获得 -- 向父级做用域寻找(注意:这种说法并不严谨,下文会重点解释).
var a = 100
function fn() {
var b = 200
console.log(a) // 这里的a在这里就是一个自由变量
console.log(b)
}
fn()
复制代码
若是父级也没呢?再一层一层向上寻找,直到找到全局做用域仍是没找到,就宣布放弃.这种一层一层的关系,就是做用域链.
var a = 100
function fn() {
var b = 200
function fn2() {
var c = 300
console.log(a) // 自由变量,顺做用域链向父做用域找
console.log(b) // 自由变量,顺做用域链向父做用域找
console.log(c) // 本做用域的变量
}
fn2()
}
fn()
复制代码
关于自由变量的值,上文提到要到父做用域中取,其实有时候这种解释会产生歧义.
var x = 10
function fn() {
console.log(x)
}
function show(f) {
var x = 20
(function() {
f() //10,而不是20
})()
}
show(fn)
复制代码
在 fn 函数中,取自由变量x
的值时,要到哪一个做用域中取?
要到建立fn
函数的那个做用域中取,不管fn
函数将在哪里调用.
因此,不要再用以上说法了.相比而言,用这句话描述会更加贴切:要到建立这个函数的那个域. 做用域中取值,这里强调的是“建立”,而不是“调用”,其实这就是所谓的静态做用域.
再看一个例子:
var a = 10
function fn() {
var b = 20
function bar() {
console.log(a + b) //30
}
return bar
}
var x = fn(),
b = 200
x() //bar()
复制代码
fn()
返回的是bar
函数,赋值给x
.执行x()
,即执行bar
函数代码.取b
的值时,直接在fn
做用域取出.取a
的值时,试图在fn
做用域取,可是取不到,只能转向建立fn
的那个做用域中去查找,结果找到了,因此最后的结果是30.
JavaScript
的执行上下文栈,能够应用堆栈信息快速定位问题执行上下文总共有三种类型:
window
对象。this
指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。函数执行上下文: 每次调用函数时,都会为该函数建立一个新的执行上下文。每一个函数都拥有本身的执行上下文,可是只有在函数被调用的时候才会被建立。一个程序中能够存在任意数量的函数执行上下文。每当一个新的执行上下文被建立,它都会按照特定的顺序执行一系列步骤。
Eval
函数执行上下文: 运行在eval
函数中的代码也得到了本身的执行上下文,但因为eval
函数不建议使用,因此在这里再也不讨论。
执行栈,在其余编程语言中也被叫作调用栈,具备 LIFO
(后进先出)结构,用于存储在代码执行期间建立的全部执行上下文。 当 JavaScript
引擎首次读取你的脚本时,它会建立一个全局执行上下文并将其推入当前的执行栈。每当发生一个函数调用,引擎都会为该函数建立一个新的执行上下文并将其推到当前执行栈的顶端。 引擎会运行执行上下文在执行栈顶端的函数,当此函数运行完成后,其对应的执行上下文将会从执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文。
this
的原理以及几种不一样使用场景的取值this
既不指向函数自身,也不指函数的词法做用域。若是仅经过 this
的英文解释,太容易产生误导了。它实际是在函数被调用时才发生的绑定,也就是说 this
具体指向什么,取决于你是怎么调用的函数。
this
的 4 种绑定规则分别是:默认绑定、隐式绑定、显式绑定、new
绑定。优先级从低到高。
什么叫默认绑定,即没有其余绑定规则存在时的默认规则。这也是函数调用中最经常使用的规则。 来看这段代码:
function foo() {
console.log( this.a );
}
var a = 2;
foo(); //打印的是什么?
复制代码
foo()
打印的结果是2。
由于foo()
是直接调用的(独立函数调用),没有应用其余的绑定规则,这里进行了默认绑定,将全局对象绑定 this
上,因此 this.a
就解析成了全局变量中的 a
,即2。
注意:在严格模式下(strict mode
),全局对象将没法使用默认绑定,即执行会报undefined
的错误
function foo() {
"use strict";
console.log(this.a);
}
var a = 2;
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined
复制代码
除了直接对函数进行调用外,有些状况是,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。
function foo() {
console.log(this.a);
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
obj.foo(); // ?
复制代码
obj.foo()
打印的结果是3。
这里foo
函数被当作引用属性,被添加到obj
对象上。这里的调用过程是这样的: 获取obj.foo
属性 -> 根据引用关系找到foo
函数,执行调用 因此这里对foo
的调用存在上下文对象obj
,this
进行了隐式绑定,即this
绑定到了obj
上,因此this.a
被解析成了obj.a
,即3。
function foo() {
console.log(this.a);
}
var a = 2;
var obj1 = {
a: 4,
foo: foo
};
var obj2 = {
a: 3,
obj1: obj1
};
obj2.obj1.foo(); //?
复制代码
obj2.obj1.foo()
打印的结果是3。
一样,咱们看下函数的调用过程: 先获取obj1.obj2
-> 经过引用获取到obj2
对象,再访问 obj2.foo
-> 最后执行foo
函数调用 这里调用链不仅一层,存在obj1
、obj2
两个对象,那么隐式绑定具体会绑哪一个对象。这里原则是获取最后一层调用的上下文对象,即obj2
,因此结果显然是4(obj2.a
)。
注意:这里存在一个陷阱,你们在分析调用过程时,要特别当心
先看个代码:
function foo() {
console.log(this.a);
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
var bar = obj.foo;
bar(); //?
复制代码
bar()
打印的结果是2。
为何会这样,obj.foo
赋值给bar
,那调用bar()
为何没有触发隐式绑定,使用的是默认绑定呢。 这里有个概念要理解清楚,obj.foo
是引用属性,赋值给bar
的实际上就是foo
函数(即:bar
指向foo
自己)。
那么,实际的调用关系是:经过bar
找到foo
函数,进行调用。整个调用过程并无obj
的参数,因此是默认绑定,全局属性a
。
function foo() {
console.log(this.a);
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
setTimeout(obj.foo, 100); // ?
复制代码
打印的结果是2。
一样的道理,虽然参传是obj.foo
,由于是引用关系,因此传参实际上传的就是foo
对象自己的引用。对于setTimeout
的调用,仍是 setTimeout
-> 获取参数中foo
的引用参数 -> 执行 foo
函数,中间没有obj
的参与。这里依旧进行的是默认绑定。
相对隐式绑定,this
值在调用过程当中会动态变化,但是咱们就想绑定指定的对象,这时就用到了显式绑定。
显式绑定主要是经过改变对象的prototype
关联对象,这里不展开讲。具体使用上,能够经过这两个方法call
或apply
来实现(大多数函数及本身建立的函数默认都提供这两个方法)。
call
与apply
是一样的做用,区别只是其余参数的设置上.
function foo() {
console.log(this.a);
}
var a = 2;
var obj1 = {
a: 3,
};
var obj2 = {
a: 4,
};
foo.call(obj1); // ?
foo.call(obj2); // ?
复制代码
打印的结果是3, 4。
这里由于显示的申明了要绑定的对象,因此this
就被绑定到了obj
上,打印的结果天然就是obj1.a
和 obj2.a
。
function foo() {
console.log(this.a);
}
var a = 2;
var obj1 = {
a: 3,
};
var obj2 = {
a: 4,
};
var bar = function(){
foo.call(obj1);
}
setTimeout(bar, 100); // 3
bar.call(obj2); // 这是多少
复制代码
前面两个(函数别名、回调函数)打印3,由于显示绑定了,没什么问题。
最后一个打印是3。
这里须要注意下,虽然bar
被显示绑定到obj2
上,对于bar
,function(){…}
中的this
确实被绑定到了obj2
,而foo
由于经过foo.call(obj1)
已经显示绑定了obj1
,因此在foo
函数内,this
指向的是obj1
,不会由于bar
函数内指向obj2
而改变自身。因此打印的是obj1.a
(即3)。
js
中的new
操做符,和其余语言中(如JAVA
)的new
机制是不同的。js
中,它就是一个普通函数调用,只是被new
修饰了而已。
使用new来调用函数,会自动执行以下操做:
JavaScrip
t对象(即{}
);this
的上下文 ;this
。从第三点能够看出,this
指向的就是对象自己。 看个代码:
function foo(a) {
this.a = a;
}
var a = 2;
var bar1 = new foo(3);
console.log(bar1.a); // ?
var bar2 = new foo(4);
console.log(bar2.a); // ?
复制代码
最后一个打印是3, 4。
由于每次调用生成的是全新的对象,该对象又会自动绑定到this
上,因此答案显而易见。
最后要注意箭头函数,它的this
绑定取决于外层(函数或全局)做用域。
闭包的概念:指有权访问另外一个函数做用域中的变量的函数,通常状况就是在一个函数中包含另外一个函数。
闭包的做用:访问函数内部变量、保持函数在环境中一直存在,不会被垃圾回收机制处理.
闭包的优势:
闭包的缺点: 由于使用闭包,可使函数在执行完后不被销毁,保留在内存中,若是大量使用闭包就会形成内存泄露,内存消耗很大
防抖和节流就是典型的闭包实际应用,还有IIFE
也是一个闭包
内存泄露:是指申请的内存执行完后没有及时的清理或者销毁,占用空闲内存,内存泄露过多的话,就会致使后面的程序申请不到内存。所以内存泄露会致使内部内存溢出
堆栈溢出:是指内存空间已经被申请完,没有足够的内存提供了
常见的内存泄露的缘由
解决方法
let flag = 0;
for(let i = 0; i < len; i++) {
flag++;
Database.save_method().exec().then((data) => {
if(flag === len) {
// your code
}
})
}
复制代码
new Promise(function(resolve){
resolve()
}).then(()=> {
for(let i = 0; i < len; i++) {
Database.save_method().exec()
}
}).then(() => {
// your code
})
复制代码
function loop(i){
i++;
Database.save_method().exec().then(() => {
loop(i)
})
}
复制代码
async
和await
(注意: 不能在forEach
中使用await
)async function loop() {
for(let i = 0; i < len; i++) {
await Database.save_method().exec();
}
}
复制代码
Module
模式在模块化规范造成以前,JS
开发者使用Module
设计模式来解决JS
全局做用域的污染问题。Module
模式最初被定义为一种在传统软件工程中为类提供私有和公有封装的方法。在JavaScript
中,Module
模式使用匿名函数自调用 (闭包)来封装,经过自定义暴露行为来区分私有成员和公有成员。
let myModule = (function (window) {
let moduleName = 'module' // private
// public
function setModuleName(name) {
moduleName = name
}
// public
function getModuleName() {
return moduleName
}
return { setModuleName, getModuleName } // 暴露行为
})(window)
复制代码
CommonJS
CommonJS
主要用在Node
开发上,每一个文件就是一个模块,没个文件都有本身的一个做用域。经过module.exports
暴露public
成员。例如:
// 文件名:x.js
let x = 1;
function add() {
x += 1;
return x;
}
module.exports.x = x;
module.exports.add = add;
复制代码
此外,CommonJS
经过require()
引入模块依赖,require
函数能够引入Node
的内置模块、自定义模块和npm
等第三方模块。
// 文件名:main.js
let xm = require('./x.js');
console.log(xm.x); // 1
console.log(xm.add()); // 2
console.log(xm.x); // 1
复制代码
// 定义AMD规范的模块
define([function() {
return 模块
})
复制代码
区别于CommonJS
,AMD
规范的被依赖模块是异步加载的,而定义的模块是被看成回调函数来执行的,依赖于require.js
模块管理工具库。固然,AMD
规范不是采用匿名函数自调用的方式来封装,咱们依然能够利用闭包的原理来实现模块的私有成员和公有成员:
define(['module1', 'module2'], function(m1, m2) {
let x = 1;
function add() {
x += 1;
return x;
}
return { add };
})
复制代码
CMD
是 SeaJS
在推广过程当中对模块定义的规范化产出。AMD
推崇依赖前置,CMD
推崇依赖就近。
define(function(require, exports, module) {
// 同步加载模块
var a = require('./a');
a.doSomething();
// 异步加载一个模块,在加载完成时,执行回调
require.async(['./b'], function(b) {
b.doSomething();
});
// 对外暴露成员
exports.doSomething = function() {};
});
// 使用模块
seajs.use('path');
复制代码
CMD
集成了CommonJS
和AMD
的特色,支持同步和异步加载模块。CMD
加载完某个依赖模块后并不执行,只是下载而已,在全部依赖模块加载完成后进入主逻辑,遇到require
语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是彻底一致的。所以,在CMD
中require
函数同步加载模块时没有HTTP
请求过程。
ES6 module
ES6
的模块化已经不是规范了,而是JS
语言的特性。随着ES6
的推出,AMD
和CMD
也随之成为了历史。ES6
模块与模块化规范相比,有两大特色:
ES6
模块输出的是值的引用。ES6
模块是编译时输出接口。模块化规范输出的是一个对象,该对象只有在脚本运行完才会生成。而 ES6
模块不是对象,ES6 module
是一个多对象输出,多对象加载的模型。从原理上来讲,模块化规范是匿名函数自调用的封装,而ES6 module
则是用匿名函数自调用去调用输出的成员。
由于时间和篇幅有限,因此每一项列举的答案都不算特别详细.
若是有这须要的同窗欢迎给我留言,我能够另开文章,详细讲一讲其中具体的部分.
固然也能够多看看《JavaScript高级程序设计》,基础才是重中之重啊.
系列连接: