你不知道的js(一)--作用域与闭包

我们知道编程语言都有变量,变量用来存储值,并能对变量的值进行修改。但是这些值存在哪里?程序如何找到它们?这需要一套设计良好的规则来存储变量,并方便的找到这些变量,这套规则被称为作用域。

比如代码

var a = 2;

js会如何解释这段代码?

编译器会将这段代码分成两个操作进行处理。首先处理var a,编译器会询问作用域是否有一个该名称的变量存在于同一个作用域的集合中,如果有,编译器会忽略该声明继续编译;否则要求当前作用域在它的集合中声明一个新的变量a。然后编译器会为执行引擎生成运行时代码,这些代码用来处理 a = 2这个赋值操作。执行引擎首先询问作用域,当前作用域集合是否存在一个变量a。如果有,引擎使用该变量,并赋值为2;如果没有,引擎会询问外层作用域,直到找到该变量或抵达最外层作用域为止。

所以,在js中作用域是可以嵌套的。例如

function foo(a) {
    console.log(a + b)
}
var b = 2
foo(2);

foo函数调用会打印4,我们分析一下foo函数的调用过程,首先执行引擎在当前作用域查找变量a,找到a为2,然后再当前作用域查找变量b,发现没有,然后引擎到外层查找变量b,发现b为2,然后将a和b的值相加为4,最后打印出相加的结果4。
因为js执行引擎查找变量是从当前作用域开始逐层向外层作用域查找,直到找到第一个匹配的变量为止,所以在多层嵌套作用域中可以定义同名的变量,这叫做遮蔽效应。但是全局变量会自动成为全局对象window的属性,因此可以通过window对象访问被遮蔽的同名变量,例如window.a访问全局变量a。但是非全局变量如果被遮蔽,则无论如何都无法被访问到。

js中有两种机制可以实现“修改”作用域。它们是eval函数和with关键字。
js中eval()函数可以接受一个字符串作为参数,并将字符串内容作为js代码执行。例如:

function foo(str, a) {
    eval(str); // 执行str文本
    console.log(a, b)
}
var b = 2;
foo("var b = 3;", 1);

打印结果不是期盼的1,2,而是1,3。原因就是foo函数调用时传入的str参数是一段js代码,并被eval函数执行,在foo函数的作用域中生成了变量b且赋值为3,遮蔽了赋值为2的全局变量b。

另一种可以修改作用域的是with关键字,with通常被当作重复引用同一个对象中多个属性的快捷方式,可以不需要重复引用对象本身。例如:

var obj = {
    a: 1,
    b: 2,
    c: 3
}
// 重复引用对象
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 快捷方式
with(obj) {
    a = 3;
    b = 4;
    c = 5;
}

考虑如下代码:

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

第一个和第二个打印结果很好理解,o1有属性a所有可以成被赋值为2,o2没有数据a所有打印结果为undefined。但值为2的a变量是从哪里冒出来的呢?其实在执行foo(o2)时,执行引擎在所有的作用域中都找不到变量a,有因为是非严格模式,所以当执行a=2时,自动创建了一个全局变量a,并赋值为2。
因为eval和with会影响到作用域的变化,所有js执行引擎在编译阶段进行代码优化时产生影响,因为变量的作用域会发生改变,所以最好不要使用它们。

一、函数作用域

函数作用域是指,属于当前函数的全部变量都可以在整个函数的范围内使用及复用,无法从外部作用域访问函数内部的任何内容。
例如:

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

foo函数的console.log打印的是函数内部的值为3的变量a,而不是值为2的变量a。

二、块作用域

也就是使用{}括起来的一段代码区域,其中定义的全部变量只能在该快作用域中使用,无法在外部作用域访问到。

  • for循环
for (var i=0i<10; i++) {
    console.log(i)
}

其中i变量只能在for循环代码块作用域中使用
- if 语句

var foo = true;
if (foo) {
    var bar = foo * 2
    console.log(bar);
}

bar变量只能在if语句块作用域中使用
js中的块作用域还有with语句块、try/catch语句块、let语句块、const语句块等

三、变量声明提升

我们先来看一段代码:

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

这段代码会打印什么?很多开发者可能认为是undefined,因为a的值被var = a;这段代码重新赋于默认值undefined了。但是运行代码后输出的结果却是2。
为什么会这样呢?这是因为编译器会将全部的声明操作在任何代码被执行前首先被处理。而且要注意的是如果同时又函数声明和变量声明,函数声明会首先被提升,然后才是变量声明。
例如:

foo();
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);
}

所以在同一个作用域中最好不要重复定义,那样会导致各种奇怪的问题。

四、闭包

当函数可以记住并访问它所定义时的作用域时,就产生了闭包,即使函数是在当前作用域之外执行。
例如:

function foo() {
    var a = 2;

    function bar() {
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz(); // 2

baz函数调用会输出2,也就是说它可以访问foo函数的内部作用域,这就是闭包的效果。
对函数类型的值进行传递,但函数在别处被调用时都可以观察到闭包。

function foo() {
    var a = 2;
    function baz() {
        console.log(a);
    }
    bar(baz);
}

function bar(fn) {
    fn();  // 2
}

如果将函数当作值类型并到处传递,就会看到闭包在这些函数中的应用。
我们还可以通过函数和闭包实现模块化编程
例如:

function MyModule() {
    var something = "cool";
    var array = [1, 2, 3];

    function func1() {
        console.log(something);
    }

    function func2() {
        console.log(array.join(","))
    }

    return {
        func1: func1,
        func2: func2
    }
}

var foo = MyModule();
foo.func1();    // cool
foo.func2();    // 1,2,3

MyModule()只是一个函数,通过调用该函数创建了一个模块实例。如果不执行外部函数,内部作用域和闭包都无法创建。通过调用MyModule()函数返回了一个对象字面量语法,从而创建了一个对象。该对象中含有MyModule()函数内部函数的引用。这样我们做到了保持模块内部数据的隐藏且私有,将返回的对象类型看做模块的公共API进行调用。
我们使用函数的apply()方法来实现一个简单的模块管理器:

var ModuleManager = (function Manager(){
    var modules = {};

    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply(impl, deps);
    }

    function get(name) {
        return modules[name];
    }
})();

ModuleManager.define("bar", [], function(){
    function hello(who) {
        return "Let me introduce:" + who;
    }

    return {
        hello: hello
    };
});

ModuleManager.define("foo", ["bar"], function(){
    var hungry = "hippo";
    function awesome() {
        console.log(bar.hello(hungry).toUpperCase);
    }
    return {
        awesome: awesome
    };
});

var bar = ModuleManager.get("bar");
var foo = ModuleManager.get("foo");

console.log(bar.hello("hippo")); // Let me introduce: hippo
foo.awesome();  // LET ME INTRODUCE: HIPPO

ES6为模块增加了语法级的支持,ES6的模块必须被定义在独立的文件中,一个文件一个模块。
例如,定义一个bar.js,文件内容如下:

function hello(who) {
    return "Let me introduce: " + who;
}

export hello;

这样我们便创建了一个名字为bar的模块。模块有一个函数hello暴露出来可以调用。

猜你喜欢

转载自blog.csdn.net/Shie_3/article/details/79776451