JS 中的作用域, 闭包, this 机制和原型往往是最难理解的概念之一. 笔者将通过几篇文章和大家谈谈自己的理解, 希望对大家的学习有一些帮助. 如果有什么理解偏差的地方, 希望大家可以评论指出, 相互学习.
有过一定编程经验的同学, 一定不会对作用域感到陌生, 在 C/C++/Java 中等语言中, 作用域从来没有 JavaScript 中的作用域那样令人困惑以致于成为一个大多数 JS 开发者都难以跨过的门槛.
作用域形成机制
JS 中存在的三种作用域类型: 全局作用域, 函数作用域和 ES6 中新加入的块级作用域.
- var a = 1;
- function foo() {
- var b = 2;
- console.log(a); // 1
- console.log(b); // 2
- console.log(c); // ReferenceError
- }
- function foo1() {
- var c = 3;
- console.log(a); // 1
- console.log(b); // ReferenceError
- console.log(c); // 3
- }
- console.log(a); // 1
- console.log(b); // ReferenceError
- console.log(c); // ReferenceError
- foo();
- foo1();
从上面的例子可以看到, 每个函数内部形成了属于自己的作用域, 函数内部声明的变量仅仅在定义的函数内部才可以访问. 全局作用域中可以访问到的有 a,foo,foo 作用域中可以访问到的有 b,foo,a,foo1 的作用域中可以访问到的有 c,foo,a. 因为 foo 的作用域嵌套在全局作用域之中, 当 console.log(a); 执行的时候, JS 在 foo 的作用域查找不到 a, 就会到它的上层 (这里是 foo 的上层直接就是全局作用域) 查找, 发现这里声明了一个 a, 将它的值打印了出来. 这种从里到外的查找就是根据作用域链查找. foo1 和 foo 的作用域没有嵌套关系, 所以相互隔离.
如果函数中使用了未声明的变量怎么办?
- function foo() {
- a = 2;
- }
- foo();
- console.log(a); // 2
JS 引擎在 foo 中查找不到 a 的声明, 便会到它的上层 (这里是全局作用域中) 查找, 这个时候还是没有查找到 a 的声明, 在非严格模式下, JS 引擎会在全局中自动声明一个 a, 这个时候, 未经声明的变量 a 实际上泄漏到了全局作用域中.
只有使用未声明的变量才会出现变量泄漏的问题么, 其实, 不仅仅这种写法会出现, 更常见的也会出现在 for 循环和 if 代码块中也会出现.
- for(var i=1;i<10;i++) {
- console.log(i);
- }
- console.log(i); // 10, 这里的 i 泄漏到了全局作用域中
- if(true) {
- var a = 2;
- }
- console.log(a); // 2, 这里的 a 也泄漏到了全局变量之中
如果你学习过 C 语言系列的语法, 往往很容易感到困惑, if 和 for 居然没有作用域, 这真是太奇怪了. 这一切的问题的根源, 都是由于 ES6 之前没有块级作用域导致的. 所以可想而知, if 包裹的代码块, 同样里面的声明也是暴露出来的~
一切问题的解决直到 ES6 中引入了 let 和 const 得以完美的解决. 使用 let 和 const, 将可以使用块级作用域, 使得声明变量泄漏的问题得以解决.
- for(let i=1;i<10;i++) {
- console.log(i);
- }
- console.log(i); // ReferenceError
- if(true) {
- let a = 2;
- }
- console.log(a); // ReferenceError
声明提升机制
对于在 JS 中声明的不论是变量还是函数, 基本上都会存在着变量声明提升的行为, 将变量的声明提升到所在作用域的顶端. ES6 中的 let 和 const 不会, 在未声明之前都不可以使用.
看看下面的代码
- console.log(a); // undefined
- console.log(b); // undefined
- console.log(foo); // Function
- console.log(foo2); // ReferenceError
- function foo () {
- console.log('声明提升了哈');
- }
- var a = 1;
- var b = function foo2() {
- console.log('不同的函数声明方式提升的结果也不一样哦');
- };
JS 引擎解释这段代码之前首先对代码中所有的变量进行了声明的提升, 函数声明的提升的优先级是高于普通变量的, 函数声明会整个提升到所在作用域的顶端(但是以函数表达式方式声明的函数不会), 代码实际上是下面这个样子:
- function foo () {
- console.log('声明提升了哈');
- }
- var a;
- var b;
- var foo2;
- console.log(a);
- console.log(b);
- console.log(foo);
- console.log(foo2);
- b = function foo2 () {
- console.log('不同的函数声明方式提升的结果也不一样哦');
- }
静态作用域机制(词法作用域)
关于 JS 中的作用域, 需要明确的一点就是, JS 中只存在静态作用域(词法作用域). 静态作用域是什么意思呢? 意思就是它的作用域在你写下代码的时候就已经确定了, 和函数的调用顺序无关, 了解这一点. 就可以对一些常见的现象进行解释.
- var a = 2;
- function foo() {
- console.log(a);
- }
- var obj = {
- a: 3,
- foo: foo
- }
- obj.foo(); // 2
foo 中的 a 在代码写完时就确认了, 指向了全局作用域中的 a, 一旦确定就无法更改了.
同理, 下面的代码
- function foo() {
- console.log(b); // ReferenceError
- }
- function foo1 () {
- var b = 1;
- foo();
- }
- foo1();
这里, JS 引擎在全局作用域中查找不到 b, 所以会抛出一个异常. 所以可以明确的道理是, foo 的作用域和 foo1 的作用域仍然是相互独立的, 不会因为调用时候的顺序而更改作用域的嵌套顺序, 静态作用域在代码书写时就已经确定无法更改了, 明白这一点在分析 JS 代码的时候尤为重要.
坑外话
变量的遮蔽效应
在函数中定义的变量会遮蔽上层作用域中同名的变量, 两个变量互不影响.
- var a = 1;
- function foo() {
- var a = 2;
- console.log(a); // 2
- }
- console.log(a); // 1
Try-Catch 中的块级作用域
try-catch 的 catch 中会创建一个块级作用域, 该作用域内变量的表现同样遵守变量的声明提升规则.
- try {
- throw undefined;
- }catch(e) {
- a = 1;
- console.log(e); // undefined
- }
- console.log(a); // 1, 变量提升规则
- console.log(e); // ReferenceError,catch 的块作用域中定义的变量
隐式声明
以参数形式传入的变量在函数内部实际上存在的隐式的声明, 使用时不算作未声明的变量.
- function foo(a) {
- a = 1;
- console.log(a);
- }
- foo(); // 1
- console.log(a); // ReferenceError
本来想一篇文章写完作用域和闭包的, 想例子实在是累, 就拆作两篇吧, 逃~
来源: https://juejin.im/post/5c8500d9e51d453b7666b376