js作用域与闭包

作用域

1、什么是作用域

作用域分为两种

  • 词法作用域(比较常见,被大多数编程语言所采用)
  • 动态作用域(仍有一些语言在用 Bash脚本、Perl中的一些模式)

2、词法作用域

词法作用域就是定义在词法阶段的作用域

欺骗词法作用域,用两种形式,一个是 eval(),另一个是 with,但是在实际的代码中我们是不建议使用的

eval

function foo (str){
    eval(str);
    console.log(a);   //a = 2;
}

foo("var a =  2");  

// ----------------严格模式下

function foo2(str){
    "use strict";
    eval(str);
    console.log(a);  //underfined
}

foo2("var a = 2")   //此时提示a is defined
/* 在严格模式下,eval()在运行时有其自己词法作用域,意味着其中的声明无法修改所在的作用域 */

在严格模式下,输出a 就会变成underfined,这是因为在严格模式下,eval()在运行时有其自己的词法作用域,也就是eval()的声明无法修改所在的作用域

with

with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身,让代码变得更加简洁方便

/* with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身 */

var obj={
    a:1,
    b:2,
    c:3
};

/* //单调乏味的重复obj
obj.a = 2;
obj.b = 3;
obj.c = 4; */

//使用with
with(obj) {
    a = 3;
    b = 4;
    c = 5;
}

console.log("a",obj.a); //3
console.log("b",obj.b); //4
console.log("c",obj.c); //5

但是使用with又会出现下面的情况

function foo(obj) {
    with(obj){
        a = 2;
    }
}

var o1 = {
    a:3
};
var o2 = {
    b:3
};
foo(o1);
console.log(o1.a);  //a =2
foo(o2);
console.log(o2.a); //undefined
console.log(a);  //a =2

创建了两个对象o1,o2,为什么输出的时候只有o1中具有a属性呢,这是因为foo(…) 函 数接受一个 obj 参数,该参数是一个对象引用,并对这个对象引用执行了 with(obj) {…},在with内部,我们将2赋值给a

当我们将 o1 传递进去,a=2 赋值操作找到了 o1.a 并将 2 赋值给它,所以log(o1.a)可以输出2,但是当o2 传递进去,o2 并没有 a 属性,因此不会创建这个属性, o2.a 保持 undefined,并且a = 2,也变成了一个全局的变量

这是因为with可以将一个没有或多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法作用域

另外一个不推荐使用eval()和 with的原因是是会被严格模式所影响(限制)。with被完全禁止,而在保留核心功能的前提下,间接或非安全使用eval(…)也被禁止了

并且如果在代码中大量使用eval()或with,那么运行起来一定会变得非常慢。

3、函数作用域和块作用域

3.1、函数中的作用域

函数作用域是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)

function foo (a){
    var b = 2;

    function bar(){

    }

    var c = 3;
}

在上面这个例子中,比如 a , b,c,还有bar都可以在函数foo内部被访问,但是在foo外部就不可以被访问

3.2、隐藏内部实现

我们在写函数的时候,经常先声明一个函数,然后再向里面添加代码。但是为什么不可以将所写的代码中挑选一个任意的片段,然后用函数声明对它进行包装,实际上就是将这些代码“隐藏”起来了

但是我们思考为什么要将这些变量和函数隐藏起来呢?

有很多原因促成了这种基于作用域的隐藏方法。它们大都是从最小特权原则中引申出来 的,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必 要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。

function dosomething(a){
    b = a+dosomethingElse(a * 2);
    console.log(b*3);
}

function dosomethingElse(a) {
    return a -1;
}

var b ;

dosomething(2);

/* 在上面这个例子中,b 和 dosomethingElse()都可以在dosomething()外部被访问,是非常不安全的 */

// ==========优化后=============

function dosomething(a) {
    function dosomethingElse(a) {
        return a -1;
    }
    var b;
    b = b + dosomethingElse(2*a);
    console.log(b * 3);
}
dosomething(b)

/* 这样写的话,b 和 dosomethingElse()就只能在dosomething()内部进行访问了 */

隐藏作用域中的变量和函数所带来的另一个好处是,可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但是用途却不一样,无意间可能会造成命名冲突。冲突会导致变量的值被意外覆盖

3.3、函数作用域

我们如何来区分函数声明和函数表达式呢,最简单的方法就是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的一个次,那么就是一个函数声明,否则就是一个函数表达式。

3.3.1 匿名和具名
setTimeout(function (){
    console.log("I want 1 second !")
},1000);

上面这个例子就是匿名函数表达式,因为function()……没有名称标识符。函数表达式可以是匿名的,而函数声明则不可以省略函数名

匿名函数子使用时简单快捷,但是也有几个缺点需要考虑

  • 匿名函数在栈追踪中不会显示出有意义的函数名,使得调式很困哪

  • 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用,

    比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑 自身

  • 匿名函数省略了对于代码可读性/可理解很重要的函数名。一个描述性的名称可以让代码不言自明

为了避免这些,我们可以这样做

setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
    
 	console.log( "I waited 1 second!" );
}, 1000 );
3.3.2 立即执行函数表达式
var a = 2;
(function foo() {
    var a = 3;
    console.log(a);
})();
console.log( a );

3.4、块作用域

try/catch

try/catch语句中catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效

try{
    undefined(); //执行一个非法操作来强制制造一个异常
}
catch(err){
    console.log(err);  // 能够正常执行!
}

console.log(err);  ReferenceError: err not found

let

Es6中引入了let关键字,提供了除var以外的另一种变量声明方式

let关键字可以将变量绑定到所在的任意作用域中(通常是{…}内部)。换句话说,let

为其声明的变量隐式了所在的块作用域,也就是let声明的变量只在所在的代码块内有效

var foo = true;
if(foo){
    let bar = foo * 2;
    bar = bar *2 - 1;
    console.log(bar);  //输出3
}

console.log(bar);   //undefined

let关键字的使用在for循环中发挥了很大的优势

for(let i = 0; i < 10;i++){
    console.log(i);
}
console.log(i); // undefined

const

除了let以外,ES6还引入了const,同样可以用来创建块作用域变量,但是const声明过后的变量是不可以修改的,这一点是与let关键字的最大的区别

var foo = true;
if (foo) {
 	var a = 2;
	const b = 3; // 包含在 if 中的块作用域常量
 	a = 3; // 正常 !
 	b = 4; // 错误 !
}
console.log( a ); // 3
console.log( b ); // ReferenceError!

任何试图修改值的操作都会引起错误

提升

a = 2;
var a ;

console.log(a);   //2
console.log(b);
var b= 2;    //undefined

大家可以观察到为什么第一个例子输出2 ,第二个反倒是undefined呢?

这是因为当执行 var b = 2 的时候,js会把它看成两个声明:var a ; 和 a = 2;第一个定义声明是在编译阶段进行的,第二个声明会被留在原地等待执行阶段。

所以我们两个代码片段分别是这样执行的

var a;
a = 2;
console.log(a);



var b;
console.log(b);
b = 2;

这个变量和函数声明从他们在代码中出现的位置被“移动”到了最上面的这个过程就叫作提升

也就是先声明后赋值

注意:只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。函数声明会被提升,但是函数表达式却不会被提升

foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
 // ...
};

这个代码片段经过提升后,实际上会被理解为以下形式:

var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
 	var bar = ...self...
 	// ...
}

函数声明和变量声明都会被提升

根据上面的案例我们可以知道函数声明还有变量声明都会被提升,但是函数会优先于变量提升前面

foo(); // 1
var foo;
function foo() {
 	console.log( 1 );
}
foo = function() {
 	console.log( 2 );
};

会输出 1 而不是 2 !这个代码片段会被引擎理解为如下形式:

function foo() {
 	console.log( 1 );
}
foo(); // 1
foo = function() {
 	console.log( 2 );
};

注意,var foo 尽管出现在 function foo()… 的声明之前,但它是重复的声明(因此被忽 略了),因为函数声明会被提升到普通变量之前。

尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。

foo(); // 3
function foo() {
 	console.log( 1 );
}
var foo = function() {
 	console.log( 2 );
};
function foo() {
 	console.log( 3 ); 
}

//输出 3 ,重复的声明会被覆盖

作用域闭包

什么是闭包呢

前面我们知道内部作用域可以获得当前作用域下的变量并且可以获得当前包含作用域的外层作用域下的变量,但是,外层作用域是无法获得内层作用域下的变量的,并且在不同的函数作用域中也是不能相互访问变量的,那如果在一个函数内部访问另一个函数内部的变量应该怎么办呢,这就是闭包的作用了 ,其本质就是有权访问另一个函数作用域中的变量的函数

我们通过一个代码案例展示一下

function foo (){
    var a = 2;

    function bar(){
        console.log(a);
    }
    return bar;
}

var baz = foo();
baz();    //2

函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作 一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。

在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),实 际上只是通过不同的标识符引用调用了内部的函数 bar()。

bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方 执行。

在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很 自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此 没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。

拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一 直存活,以供 bar() 在之后任何时间进行引用。

bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

function foo(){
    var a = 2222;

    function baz(){
        console.log(a);
    }

    bar (baz);
}

function bar (fn){
    fn();
}
foo();

通过上面的例子,我们就容易理解了闭包的三个特性

  1. 函数嵌套函数
  2. 函数内部可以引用函数外部的参数和变量
  3. 参数和变量不会被垃圾回收机制回收

常见的闭包

function wait (message) {
    
    setTimeout(function timer(){
        console.log(message);
    }, 1000 );
}
wait("Hello,word!");

将一个内置函数(名为timer)传递给setTimeout()。timer具有涵盖wait( … ) 作用域的闭包,因此还保有对变量message的引用

循环和闭包

for( var i = 1; i <= 5; i++){
    setTimeout(function timer(){
        console.log(i);
    },i*1000);
}
// 6
// 6
// 6
// 6
// 6

大家可能会好奇,为什么上面的循环以每隔一秒输出五个6呢?

这个循环的终止条件是 i不再<=5,也就是i = 6 的时候终止循环。而延迟函数的回调会在循环结束时才执行,这也就导致了每次都输出一个6出来。

尽管循环中的五个函数是在各个迭代中分别定义的, 但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。

那我们如何改变这种状况呢

它需要有自己的变量,用来在每个迭代中储存 i 的值:

for (var i = 1; i<=5 ;i++){
    (function (){
        var j = i;
        setTimeout(function timer(){
            console.log(j);
        },j*1000);
    })();
}
// 1
// 2
// 3
// 4
// 5

好像前面我们所说过的块作用域也是可以的吧,那我们试一下

for (let i=1; i<=5; i++) {
 	setTimeout( function timer() {
 	console.log( i );
 	}, i*1000 );
}
// 1
// 2
// 3
// 4
// 5

是的,它是可以的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值