一、作用域
作用域,是指变量的生命周期。
- 全局作用域
- 函数作用域
- 块级作用域
- 词法作用域
- 动态作用域
1、全局作用域
全局变量:生命周期将存在于整个程序之内。能被程序中任何函数或者方法访问。在javascript内默认是可以被修改的。
1.1显示声明
带有关键字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对象上。
2、函数作用域
function f(){
var a = 1;
}
console.log(a);复制代码
2.1如何访问函数作用域内的变量呢?
法一:通过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的区别。
法二、通过闭包访问函数内部的变量
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);复制代码
3、块级作用域
在 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);
}复制代码
法三、立即执行函数
for(var i = 0; i < 3; i++){
(function(j){
setTimeout(function(){
console.log(j);
},200)
})(i);
}复制代码
立即执行函数同函数调用,先把for循环中的i记录下来,然后把i赋值给j,然后输出0,1,2。
4、词法作用域
函数的作用域在函数定义的时候就决定了。
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();复制代码
通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量如果被遮蔽了,无论如何都无法被访问到。
5、动态作用域
而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。
动态作用域,作用域是基于调用栈的,而不是代码中的作用域嵌套;
请听面试题:
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。
二、作用域链
每一个 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 个),这样的查找到我们需要的变量会引发多大的性能问题,所以,尽量将 全局变量局部化 ,避免作用域链的层层嵌套。
理解执行上下文
- 在函数未调用之前,add 函数的
[[Scope]]
属性的作用域链里面已经有以上内容。 - 当执行此函数时,会建立一个称为 执行上下文 (execution context) 的内部对象。一个 执行上下文 定义了一个函数执行时的环境,每次调用函数,就会创建一个 执行上下文 ;一旦初始化 执行上下文 成功,就会创建一个 活动对象 ,里面会产生
this
arguments
以及我们声明的变量,这个例子里面是x
、y
。
结束执行上下文阶段
作用域链和执行上下文的关系
作用域链本质上是指向一个指针,指向变量对象列表。创建函数时,会把全局变量对象的作用域链添加在[[Scope]]属性中。调用函数时,会为函数创建一个执行环境的作用域链,并且创建一个活动对象,并将其推入执行环境作用域链的前端。函数局部环境的变量对象只有在函数执行的过程中才存在。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是闭包的情况有所不同。
三、闭包
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。