03.变量作用域

123 阅读5分钟

1. var声明 vs 直接声明

var number = 999; // var 声明
number = 999// 直接声明,严格模式下,会抛出异常
  • var方法声明的变量符合链式作用域
  • 直接声明的变量,也就是不加var关键字声明的便令会被直接添加到global(浏览器中即window)对象中

2. 变量的链式作用域(Chain Scope)

链式作用域:子对象会一级一级的向上寻找所有父对象的变量。

也就是说,父对象的所有变量对子对象可见,但是子对象的变量对父对象不可见。这是一个非常通俗的解释,详细的内存管理方式需要参加这篇文章:blog.csdn.net/whd526/arti…

Demo

function parent() {
    var number1 = 999;

    function child() {
        console.log(number1); // 999

        var number2 = 1000;
    }

    console.log(number2) // error, cannot access variable declared in child object.
}

3. javascript{}作用域

javascript一个非常重要的特点是,javascipt在设计之初是没有{}作用域的。简单来说,在一个函数内部,如果你在一个if分支中用var关键字声明了变量,那么,在if分支外部,这个变量依然可以访问。

var condition = true;

if(condition) {
    var varNumber = 999;
}

console.log(varNumber); // 999

上面代码中,varNumber虽然是在if(condition)中定义的,但是在if分支外部,依然可以访问,这是因为javascript中没有{}作用域。

4. let关键字的引入

上面一点提到过,javascript在设计之初是没有{}作用域的,之所以强调在设计之初,是因为,C/C++/C#/java之类的语言是存在{}作用域的,而且愈来愈多的这类程序员使用javascript开发,因此,为了方便大家使用,在es6中引入了let关键字。

let关键字支持了{}作用域,我们可以看一下一下的demo

// es6 
let number = 999;

if(number > 0) {
    let number = 1000;
    console.log(number); // 1000
}

console.log(number); // 999
child();
// 编译为es5后的代码
var number = 999;

if (number > 0) {
    var _number = 1000;
    console.log(_number); // 1000
}

console.log(number); // 999

之所以能够将let声明的变量限制在{}作用域中,是因为es6->es5之后,声明了一个新的变量。

5. 闭包(closure)

个人理解闭包有两个方面的作用,还有一点需要注意的方面。

闭包:由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成定义在一个函数内部的函数

function closureDemo() {
    var n = 999;
    function display() {
        console.log(n);
    }
    function increase() {
        n++;
    }
    return { display, increase };
}

var result = closureDemo();
result.display(); // 999
result.increase(); // n++
result.display(); // 1000
  • 闭包的两点作用
    • 读取其他函数定义的变量
    • 读取的其他函数内部定义的变量的值一直驻留在内存中

      其实这点非常重要,第一点可以通过函数返回值也可以做到,但是有了第二点,就不一样了,因为在使用闭包时,返回的函数一直处于被引用状态,所以不会被垃圾回收器回收,因此,会一直得到其内部变量在内存中的变化值。

      向上面示例代码中的例子,变量n一直驻留在内存中,因为result对象,一直处于引用状态,所以,可以看到n999变到了1000

  • 闭包要注意的问题

    但是也是因为第二点,闭包返回的函数以及闭包内声明的变量一直驻留在内存中,所以闭包不能被滥用,否则会导致内存溢出,闭包的使用应该谨慎。

  • Demo
// closures.js
function closureVariableTest1() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function () {
            return i;
        };
    }
    return result;
}

function closureVariableTest2() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = (function () {
            var num = i;
            return num;
        })(i)
    }
    return result;
}

var result1 = closureVariableTest1();
var result2 = closureVariableTest2();

for (var i = 0; i < result1.length; i++) {
    console.log(result1[i]()); // 10, 10, 10, 10, 10, 10, 10, 10, 10, 10
    /**
     * Since there is no brace scope for var, so, result1's element will store the function,
     * and when the function executed, the i is always 10.
     */
}

for (var i = 0; i < result2.length; i++) {
    console.log(result2[i]); // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
    /**
     * Every element store the execute result of the function, so it is expected.
     */
}

上面的例子中,closureVariableTest1()中,所有的element都显示10,因为数组中保存的是一个function,当这个function被调用的时候,i已经变化为10了,所以,数组的每个元素被调用的时候,显示的都是10

closureVariableTest2()中,保存元素时,已经将i这个数字赋值给数组的每个元素,因此,无论何时调用数组元素,都会输出正确的数组下标。

6. this指针

了解了闭包后,this指针的指向问题就很好理解了。我理解,简单来说就是两点:

  • 那个对象调用了函数,函数内部的this对象就指向哪个对象

  • 在严格模式下,函数内调用this,将为undefined

    'use strict';
    
    function foo() {
        try {
            console.log(this);// undefined
        }
        catch (err) {
            console.log(err);
        }
    }
    
    foo();
    
  • 在非严格模式下,匿名函数中的this指针指向window对象

    function useThisInsideObject() {
        name = 'The Window';
        var object = {
            name: 'My Object',
            getNameFunc: function () {
                return function () {
                    return this.name; 
                }
            }
        };
    
        console.log(object.getNameFunc()()); 
        /**
         * `The Window`, since object.getNameFunc()()将调用getnameFunc中的匿名函数.
         * 匿名函数中的this指针将会指向window对象,window.name = "The Window".
        */
    }
    
    function keepTheContextOfObject() {
        name = 'The Window';
        var object = {
            name: 'My Object',
            getNameFunc: function () {
                var self = this;
                return function () {
                    return self.name;
                }
            }
        };
    
        console.log(object.getNameFunc()());
        /**
         * `My Object`因为在调用匿名函数前,已经使用self记录了this对象的状态,所以self.name的值为`My Object`.
        */
    }
    
    useThisInsideObject();
    keepTheContextOfObject();
    

7. Call, Apply & Bind

通过CallApplyBind方法可以改变this指针的指向。他们之间的区别在于:

方法名参数接收返回值
call可变数组
e.g.
Call(object, arg1, arg2, arg3, ...)
Call(object, [arg1, arg2, arg3, ...])
都是正确的
返回结果值
apply数组
e.g.
Call(object, arg1, arg2, arg3, ...) // 错误
Call(object, [arg1, arg2, arg3, ...]) //正确
返回结果值
bind参数列表
e.g.
//正确
bind(object, arg1, arg2, arg3, ...)
// 不会错误,但是会把数组当做对象处理,不会当做可变数组
bind(object, [arg1, arg2, arg3, ...])
返回binding了新对象的函数
// callBindApply.js
var user = {
    user_name: "yafeya",
    speak: function () {
        console.log(this.user_name);
    },
    hello: function(word){
        console.log(`${this.user_name} said ${word}`);
    }
}

user.speak(); // yafeya
var speak = user.speak;
var hello = user.hello;
speak(); // undefined, 因为user.speak()被赋值给了speak对象,这里的speak对象是一个匿名函数,所以其中的this指向window对象。

console.log('call & apply');
speak.call(user); // yafeya, call方法将this指针指向user对象.
speak.apply(user); // yafeya, apply方法将this指针指向user对象.

console.log('call & apply with parameters');
hello.call(user, 'hello'); // call方法可以接受可变数组
hello.apply(user, ['hello']); // apply方法只能接受数组,如果填写可变数组,将会抛出异常.

console.log('bind demo')
hello.bind(user, 'hello')(); // yafeya, bind方法接收可变数组,返回binding了新对象的函数.