JavaScript 变量作用域、this、闭包

简介: JavaScript 变量作用域、this、闭包

变量作用域 scope 和作用域链 scope chain


var 变量的作用域和变量提升


JavaScript 有两种作用域:全局和局部。

在函数定义之外声明的变量是全局变量,它的值可在整个程序中访问和修改。

在函数定义内声明的变量是局部变量。每当执行函数时,都会创建和销毁该变量,且无法通过函数之外的任何代码访问该变量。

JavaScript 中,在函数体内 var 声明的变量是函数级作用域,是局部变量,在本函数体内可以访问,而且是在函数体内任意位置可以访问。

// JS 代码
function test() {    
    console.log(val);
    var val = 'this is val';
    console.log(val);
    func();    
        function func() {        
        for (var i = 0; i < 5; i++) {
        }
        console.log('i: ', i);
        console.log('this is func');
    }
}
test();

上述代码结果是:

undefined this is val
i:  5this is func

JavaScript 解析器预解析代码的时候, test 函数作如下解析:

function test() {    // 变量提升, 缺省值是 undefined
    var val;    // 函数声明提升
    function func() {        // 变量提升
        var i;
        for (i = 0, i < 5, i++) {
        }        console.log('i: ', i);
        console.log('this is func');
    }
        console.log(val);    // 变量赋值
        val = 'this is val';
        console.log(val);
    func();
}

所以第一次 console.log(val) 时候并不会抛异常, 因为此时变量 val 是被声明过的,值是 undefined

理解 变量提升 ,写代码时应注意 变量污染 的坑。

如果声明变量时不加 var 直接 val = 1;, 那么 val 是全局变量。


作用域链


作用域链包含了执行环境有权访问的所有变量和访问顺序。

作为单线程语言的 JavaScript,初始化代码时会创建一个全局上下文,每一次函数调用都会创建一个执行上下文,执行上下文及包含关系:

  • 变量对象
  • 变量
  • 函数声明
  • 参数(arguments)
  • 作用域链
  • 有权访问的变量和访问顺序(本作用域变量和所有父作用域变量)。即函数内部属性 scope : 本函数有权访问的[变量、对象、函数]的集合
  • this 值

如下代码:

function func_1() {   
 var val_1 = 1;    
     // 抛异常: ReferenceError: val_2 is not defined
    console.log(val_1, val_2);    
     function func_2() {
        var val_2 = 2;        // 输出:1 2        
        console.log(val_1, val_2);
    }
    func_2();
}
func_1();

简言之, func_1 不能访问 func_2 中声明的变量, func_2 可以访问 func_1 中声明的变量。

当在作用域内访问一个变量 x 时,JavaScript 的查找顺序是这样的:

  1. 当前作用域 var x 的定义 => 2. x 形参 => 3. 函数自身名称是否是 x => 4. 上级作用域从 1 开始查找


ES6 中的 let 和 const


ES6 的 let 和 const 实现了块级所用域的变量声明方式,使用 let 和 const 声明变量能有效避免由于变量提升导致的变量污染的问题。

用 let 和 const 声明的变量作用域是代码块,这个设计比较符合大多数人的思维方式。(代码块简单来说就是 {} 大括号包着的区域)

function test() {
    if (true) {
            var a = 'a';
            let b = 'b';
    }
    // 输出: a
    console.log(a);
    // 抛异常:ReferenceError: b is not defined
    console.log(b);
}
test();

关于 const 的作用有必要正确理解:

严格来说, const 声明了一个指向变量的指针,并不是说 const 声明的变量不可改变, 而是该指针指向的地址不可改变。

MDN 的例子很赞,这里直接拷过来看:

简言之: this 总是指向调用该函数的对象。


全局上下文 global context


// 在浏览器中console.log(this === window); // true


函数上下文 function context


在函数中访问 this 时, this 指向调用该函数的对象。

1)全局对象

// 全局变量val = 1;function test() {
    console.log(this.val);
}
test(); // 1

上例中,调用 test 函数的对象并不是一个自己声明的函数或对象,此时 this 默认值为全局对象。

2) 调用对象

var testObj = {
    val: 1,
    getVal: function() {
        var val = 2;
        return this.val;
    }
};
console.log(testObj.getVal()); // 1

上述代码运行输出 1, 顺藤摸瓜,getVal() 函数的调用者是 testObj 对象, 按照 this 指向调用该函数的对象 的原则,getVal() 中的 this 指向 testObj 对象, testObj 对象的 val 值是 1.

3) 构造函数

'use strict';
function testFunc(val) {
    this.a = val;
    this.b = 'bb';
}
 var testInstance = new testFunc('aa');
 console.log(testInstance.a); // aa
 console.log(testInstance.b); // bb

当一个函数的调用者是构造函数(new 出来的对象), this 指向新构造出来的对象 testInstance

4) call and apply

通过 call apply 将 this 指向特定对象:

function testFunc(val) {
    this.a = val;
    this.b = 'bb';
}
function execFunc() {
    var a = 'exec aa';
    var b = 'exec bb';
    console.log(this.a, this.b);
}
var testInstance = new testFunc('aa');
execFunc.call(testInstance); // aa bb
execFunc.apply(testInstance); // aa bb

通过 call apply 函数将 execFunc 的 this 值指向 testInstance 对象的 this 值。

注意: 以 fun.apply() // or call 为例 call apply 的第一个参数是 func 函数运行时的 this 值 (第一个参数的解释版本真的多)。

二者的区别这里不说。


补充:箭头函数、严格模式下的 this


1) ES6 中的箭头函数 arrow function

GLOBAL.a = 'global aa';var testObj = {
    a: 'aa',
    getValArrowFuc: function() {
        var val = (() => this.a);
        return val();
    },                                 
    getVal: function() {
        var self = this;
        var val = function() {
        return self.a;
        };
        return val();
    },
    getValGlobal: function() {
        var val = function() {
        return this.a;
        };
        return val();
    }
};
console.log(testObj.getValArrowFuc()); // aa
console.log(testObj.getVal()); // aa
console.log(testObj.getValGlobal()); // global aa

箭头函数中的 this 值,就是词法作用域的 this 值。

2) 严格模式下的 this

对于一个开启严格模式的函数,指定的 this 不再被封装为对象,而且如果没有指定 this 的话它值是 undefined.

'use strict';/**
 * from MDN
 */function fun() { return this; }
console.log(fun() === undefined); // true
console.log(fun.call(2) === 2); // true
console.log(fun.apply(null) === null); // true
console.log(fun.call(undefined) === undefined); // true
console.log(fun.bind(true)() === true); // true

注: 以上所有不考虑 Eval


闭包 closure


闭包的构成:

  • 函数
  • 创建该函数的环境,环境由闭包创建时在作用域中的任何局部变量组成

自执行函数表达式写法:

var test = (function() {
    var val = 0;
        var add = function(num) {
        val += num;
        return val;
    };
        return add;
})();
console.log(test(3)); // 3
console.log(test(4)); // 7

个人感觉这个写法可读性更好:

var test = function() {
    var val = 0;
        var add = function(num) {
        val += num;
        return val;
    };
        return add;
};
 /**
 * 此处 instance 是一个闭包。
 * 由 add 函数, 和创建 add 函数时的环境(变量 val)组成
 */
var instance = test();
console.log(instance(3)); // 3
console.log(instance(4)); // 7

以下代码:

function test() {
    for (var i = 0; i < 5; i++) {
        setTimeout(() => {
           console.log(i);
        }, 10);
    }
}
test();

输出是:

5
5
5
5
5

这里变量 i 的作用域是 test 函数作用域,也就是说 console.log(i) 中的 i 是 test 函数作用域下的同一个变量。

setTimeout 中的函数被执行时,for 遍历已完成并且 i 被赋值为 5.

利用闭包:

function test() {
    for (var i = 0; i < 5; i++) {
        (function (val) {
            setTimeout(() => {
                    console.log(val);     
            }, 10);
        })(i);
    }
}
test();

则会输出:

0
1
2
3
4

这里我们将 i 赋值成一个局部变量,可在闭包内访问(每次循环创建一个闭包, i 作为该闭包作用域下的局部变量,不跟随外层 i 的值改变)。

闭包对性能有负面影响(尤其是内存占用),如果不需要使用,则不使用。


目录
相关文章
|
1月前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
2月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
1月前
|
JavaScript 前端开发
js 闭包的优点和缺点
【10月更文挑战第27天】JavaScript闭包是一把双刃剑,在合理使用的情况下,它可以带来很多好处,如实现数据封装、记忆功能和模块化等;但如果不注意其缺点,如内存泄漏、变量共享和性能开销等问题,可能会导致代码出现难以调试的错误和性能问题。因此,在使用闭包时,需要谨慎权衡其优缺点,根据具体的应用场景合理地运用闭包。
114 58
|
1月前
|
自然语言处理 JavaScript 前端开发
[JS]作用域的“生产者”——词法作用域
本文介绍了JavaScript中的作用域模型与作用域,包括词法作用域和动态作用域的区别,以及全局作用域、函数作用域和块级作用域的特点。通过具体示例详细解析了变量提升、块级作用域中的暂时性死区等问题,并探讨了如何在循环中使用`var`和`let`的不同效果。最后,介绍了两种可以“欺骗”词法作用域的方法:`eval(str)`和`with(obj)`。文章结合了多位博主的总结,帮助读者更快速、便捷地掌握这些知识点。
35 2
[JS]作用域的“生产者”——词法作用域
|
1月前
|
前端开发 JavaScript
如何在 JavaScript 中访问和修改 CSS 变量?
【10月更文挑战第28天】通过以上方法,可以在JavaScript中灵活地访问和修改CSS变量,从而实现根据用户交互、页面状态等动态地改变页面样式,为网页添加更多的交互性和动态效果。在实际应用中,可以根据具体的需求和场景选择合适的方法来操作CSS变量。
|
1月前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
|
1月前
|
JavaScript 前端开发
如何在 JavaScript 中实现块级作用域?
【10月更文挑战第29天】通过使用 `let`、`const` 关键字、立即执行函数表达式以及模块模式等方法,可以在JavaScript中有效地实现块级作用域,更好地控制变量的生命周期和访问权限,提高代码的可维护性和可读性。
|
1月前
|
JavaScript 前端开发
javascript的作用域
【10月更文挑战第19天javascript的作用域
|
1月前
|
前端开发 JavaScript UED
如何使用 JavaScript 动态修改 CSS 变量的值?
【10月更文挑战第28天】使用JavaScript动态修改CSS变量的值可以为页面带来更丰富的交互效果和动态样式变化,根据不同的应用场景和需求,可以选择合适的方法来实现CSS变量的动态修改,从而提高页面的灵活性和用户体验。
|
1月前
|
缓存 JavaScript 前端开发
js 闭包
【10月更文挑战第27天】JavaScript闭包是一种强大的特性,它可以用于实现数据隐藏、记忆和缓存等功能,但在使用时也需要注意内存泄漏和变量共享等问题,以确保代码的质量和性能。
38 7