做用域,是指变量的生命周期。javascript
一、全局做用域前端
全局变量:生命周期将存在于整个程序以内。能被程序中任何函数或者方法访问。在javascript内默认是能够被修改的。java
1.1显示声明node
带有关键字var的声明:面试
var a = 1;
var f = function(){
console.log("come on");
};
//全局变量会挂在到window对象上
console.log(window.a);
console.log(window.f);复制代码
1.2隐式声明编程
function f(num){
result = num + 1;
return result;
}
f(1);
console.log(window.result);复制代码
不带有var关键字的变量result,也被隐式声明为一个全局变量。挂载在window对象上。数组
二、函数做用域浏览器
function f(){
var a = 1;
}
console.log(a);复制代码
2.1如何访问函数做用域内的变量呢?bash
法一:经过return返问函数内部变量闭包
function f(num){
var result = num++;
return result;
}
console.log(f(1));复制代码
function f(num){
var result = num + 1;
return result;
}
console.log(f(1));复制代码
function f(num){
var result = ++num;
return result;
}
console.log(f(1));复制代码
以上三段程序也体现了i++和++i的区别。
法2、经过闭包访问函数内部的变量
function outer(){
var value = 'inner';
return function inner(){
return value;
}
}
console.log(outer()());复制代码
2.2当即执行函数
当即执行函数可以自动执行(function(){})()里面包裹的内容,可以很好地消除全局变量的影响。
(function(){
var a = 1;
var foo = function(){
console.log("haha");
}
})();
console.log(window.a);
console.log(window.foo);复制代码
三、块级做用域
在 ES6 以前,是没有块级做用域的概念的。
for(var i = 0; i < 3; i++){
}
console.log(i); 复制代码
很明显,用 var 关键字声明的变量,存在变量提高,至关于:
var i;
for(i = 0; i < 3; i++){
}
console.log(i); 复制代码
若是须要实现块级做用域,可使用let关键字,let关键字是不存在变量提高的。
for(let i = 0; i < 3; i++){
}
console.log(i);
复制代码
一样能造成块级做用域的还有const关键字。
if(true){
const a = 1;
}
console.log(a);复制代码
块级做用域的做用以及常考的面试题
for(var i = 0; i < 3; i++){
setTimeout(function(){
console.log(i);
},200);
}复制代码
为何i是3呢?
缘由由于var声明的变量能够进行变量提高,i是在全局做用域里面的,for()循环是同步函数,setTimeout是异步操做,异步操做必须等到全部的同步操做执行完毕后才能执行,执行异步操做以前i已是3,因此以后会输出同一个值3。
如何让它按咱们想要的结果输出呢?
法一:最简单使用let
for(let i = 0; i < 3; i++){
setTimeout(function(){
console.log(i);
},200);
}
复制代码
法二:调用函数,建立函数做用域;
for(var i = 0; i < 3; i++){
f(i);
}
function f(i){
setTimeout(function(){
console.log(i);
},200);
}复制代码
法3、当即执行函数
for(var i = 0; i < 3; i++){
(function(j){
setTimeout(function(){
console.log(j);
},200)
})(i);
}复制代码
当即执行函数同函数调用,先把for循环中的i记录下来,而后把i赋值给j,而后输出0,1,2。
四、词法做用域
函数的做用域在函数定义的时候就决定了。
var value = 'outer';
function foo(){
var value = 'middle';
console.log(value); //middle
function bar(){
var value = 'inner';
console.log(value); //innner
}
return bar();
}
foo();
console.log(value); //outer复制代码
当咱们要使用声明的变量时:JS引擎总会从最近的一个域,向外层域查找;
例:面试题
var a = 2;
function foo(){
console.log(a);
}
function bar(){
var a = 3;
foo();
}
bar();复制代码
若是是词法做用域,也就是如今的javascript环境。变量a首先会在foo()函数里面查找,若是没有找到,会根据书写的位置,查找上一层的代码,在这里是全局做用域,找到并赋值为2,因此控制台输出2。
咱们说过,词法做用域是写代码的时候就静态肯定下来的。Javascript中的做用域就是词法做用域(事实上大部分语言都是基于词法做用域的),因此这段代码在浏览器中运行的结果是输出 2
。
做用域的"遮蔽"
做用域查找从运行时所处的最内部做用域开始,逐级向外或者说向上进行,直到碰见第一个匹配的标识符为止。在多层的嵌套做用域中能够定义同名的标识符,这叫做“遮蔽效应”,内部的标识符“遮蔽”了外部的标识符。
var a = 0;
function test(){
var a = 1;
console.log(a);//1
}
test();复制代码
var a = 0;
function test(){
var a = 1;
console.log(window.a);//0
}
test();复制代码
经过这种技术能够访问那些被同名变量所遮蔽的全局变量。但非全局的变量若是被遮蔽了,不管如何都没法被访问到。
五、动态做用域
而动态做用域并不关心函数和做用域是如何声明以及在何处声明的,只关心它们从何处调用。
动态做用域,做用域是基于调用栈的,而不是代码中的做用域嵌套;
请听面试题:
var x = 3;
var y = 3;
function A(y){
var x = 2;
var num = 3;
num++; //4
function B(num){
return x * y * num; //x,y,num:1,5,4
}
x = 1;
return B;
}
console.log(A(5)(4));
复制代码
解析:
本题的关键在于肯定x的值,函数B是在 return B;时执行的,因此x的值在函数调用前已经修改成1;因此返回20。
2、做用域链
每个 javaScript 函数都表示为一个对象,更确切地说,是 Function 对象的一个实例。
Function 对象同其余对象同样,拥有可编程访问的属性。和一系列不能经过代码访问的属性,而这些属性是提供给 JavaScript 引擎存取的内部属性。其中一个属性是 [[Scope]] ,由 ECMA-262标准第三版定义。
内部属性 [[Scope]] 包含了一个函数被建立的做用域中对象的集合。
这个集合被称为函数的 做用域链,它能决定哪些数据能被访问到。
来源于:《 高性能JavaScript 》;
例:
function add(x,y){
return x + y;
}
console.log(add.prototype);
复制代码
[[Scope]]
属性下是一个数组,里面保存了,做用域链,此时只有一个 global
。
理解词法做用域的原理
var a = 2;
function foo(){
console.log(a); //2
console.log(foo.prototype);
}
function bar(){
var a = 3;
console.log(a); //3
foo();
console.log(bar.prototype);
}
bar();复制代码
node环境下:
浏览器下:
疑惑:为何在node中scopes数组中有两个对象,在浏览器中scopes数组中只有一个对象。
缘由:node的模块化,本质上也是在外层添加一个匿名函数,由于node的模块化,在编译的时候,给每一个JS文件外部包裹一个匿名函数。因此会出现scopes中有两个对象。展开scopes[0],会发现里面确实包含在浏览器中是全局的变量或全局的函数,而在node环境下因为多包裹了一层匿名函数,会让它存在于closure中。
var a = 2;
function bar(){
var a = 3;
console.log(a); //3
foo();
console.log(bar.prototype);
function foo(){
console.log(a); //3
console.log(foo.prototype);
}
}
bar();复制代码
node环境下:
浏览器下:
全局做用域链是在全局执行上下文初始化时就已经肯定了。
证实:
console.log(add.prototype); //1声明前
function add(x,y){
console.log(add.prototype); //2运行时
return x + y;
}
add(1,2);
console.log(add.prototype); //3执行后
复制代码
1声明前
2运行时
3执行后
做用域链是在 JS 引擎完成初始化执行上下文环境就已经肯定了。
理解做用域链的好处:若是做用域链越深, [0] => [1] => [2] => [...] => [n],咱们调用的是全局变量,它永远在最后一个(这里是第 n 个),这样的查找到咱们须要的变量会引起多大的性能问题,因此,尽可能将 全局变量局部化 ,避免做用域链的层层嵌套。
理解执行上下文
[[Scope]]
属性的做用域链里面已经有以上内容。this
arguments
以及咱们声明的变量,这个例子里面是 x
、y
。结束执行上下文阶段
做用域链和执行上下文的关系
做用域链本质上是指向一个指针,指向变量对象列表。建立函数时,会把全局变量对象的做用域链添加在[[Scope]]属性中。调用函数时,会为函数建立一个执行环境的做用域链,而且建立一个活动对象,并将其推入执行环境做用域链的前端。函数局部环境的变量对象只有在函数执行的过程当中才存在。通常来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域。可是闭包的状况有所不一样。
3、闭包
function createComparisonFunction(propertyName){
return function(object1,object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
}
//建立函数
var compareNames = createComparisonFunction('name');
//调用函数
var result = compareNames({name:"Nicholas"},{name:"Greg"});
//解除对匿名函数的引用
compareNames = null;复制代码
调用compareNames()
函数的过程当中产生的做用域链之间的关系图以下
createComparisonFunction执行完毕后,其执行环境的做用域链会被销毁,但其活动对象不会被销毁,由于匿名函数的做用域链还在引用这个活动对象。直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。
闭包与变量
function createFunctions(){
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
console.log(createFunctions()[0]()); //10复制代码
实际上每一个函数都会返回10。缘由:由于每一个函数的做用域链中都会保存着createFunctions()函数的活动对象,因此它们引用的都是同一个变量。
如何让闭包输出想要的结果呢?
function createFunctions(){
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
}
}(i);
}
return result;
}
for(var j = 0; j < 10; j++){
console.log(createFunctions()[j]());
}复制代码
理解:咱们没有直接把闭包赋值给数组,而是定义了一个匿名函数,而后给该匿名函数传入参数,因为参数是按值传递的,因此至关于把当前的i赋值给num,再在该匿名函数内生成一个闭包,返回num。