作用域详解

1.作用域是什么?

作用域,也叫做静态作用域。是变量存在的范围,或者说查找变量的范围。

作用域之所以是静态作用域,是因为一旦声明完成,作用域就不再变化(eval除外)。

js运行时,查找变量是通过作用域链查找,从声明时所在的作用域开始查找。

2.如何作用?

在引擎运行时,通过编译器的结果协助引擎查询变量。

代码编译通常有 词法分析,语法分析(AST), 生成可执行代码

引擎查询时分为两种,

一种是LHS查询。主要是查询在赋值操作符左侧的变量,等待赋值,其中函数传参是隐形的LHS查询;

另一种是RHS查询。主要是查询并获取变量的值。

区别:

RHS查询失败,直接抛出ReferenceError;

LHS非严格模式下,自动创建全局变量;严格模式下,抛出ReferenceError异常。

function foo(a) {
    var b = a;
    return a + b;
}
var a = foo(2);

// 含有3个LHS查询,4个RHS查询
// 3个LHS: a = , a参数(隐式), b=..
// 4个RHS:foo, =a, a +, +b

3. 作用域嵌套

查找遍历嵌套作用域的规则是引擎从声明时所在作用域开始查找,如果找不到,向上一级作用域请求,直到全局作用域。

如果找不到,引擎抛出异常ReferenceError。

下图是个嵌套作用域。

1是全局作用域,包含标识符foo;

2是foo创建的作用域,包含标识符a, b,bar;

3是bar创建的作用域, 包含标志符c;

4.词法作用域

词法作用域也就是词法阶段的作用域。由写代码时的位置决定。一般不变。

无论函数在哪里被调用,如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

词法作用域只能查找一级标识符,如a,b,c

对于访问foo.bar,baz。词法作用域只能查找foo,剩下的通过对象属性访问规则访问。

欺骗词法作用域

对于大多数情况来说,代码写完,词法作用域就不再变化。但也有例外,可以在运行时修改(欺骗)词法作用域。

⚠️最好不使用。会导致性能下降。并且在严格模式下失效。

实现机制有两种:

1)eval()

eval(str)函数接收一个字符串作为参数,并将其中的内容看作书写时就存在的代码。

作用域是当前作用域;但是非直接调用eval时,作用域就是widow;

但是不能是'return;'这种和其他代码配合的命令。

function foo(str, a) {
    eval(str); //  欺骗--相当于var b = 3;
    console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1); // 1, 3

function foo(str, a) {
    "use strict"
    eval(str); // 严格模式下,eval有自己的作用域
    console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1); // 1, 2

2) with(已被禁止不做考虑)

性能下降原因:

js引擎在编译阶段会对作用域查找进行优化。预先确定变量和函数的定义位置,快速找到标志符。

但是如果使用了eval(), 引擎会默认所有标识符的位置都不确定,则所作的优化无效。

尽量不要使用eval()

5. 函数作用域

函数作用域是函数定义时所在的作用域,而不是函数调用时所在的作用域。

RHS查询从当前作用域(定义时所在的作用域)开始查找。

// 函数在外部声明的,作用域绑定在外部
var x = function () {
  console.log(a);
};
function y(f) {
  var a = 2;
  f(); 
}
y(x); // ReferenceError: a is not defined
// 函数在内容声明的,作用域绑定在内部
var a = 2;
function foo() {
  var a = 1;
  function bar(){
    console.log(a);
  }
  return bar;
}
foo()();

函数作用域中属于这个函数的所有标志符(变量、函数)都能在函数范围内使用和复用。但是函数外部不能使用。

function a() {
  function b(){}
}
b(); // ReferenceError: b is not defined

隐藏内部实现

根据函数作用域的规定,可以将一些代码包裹在一个声明的函数中,使外部无法访问,达到“隐藏”代码的目的。

必要性:

依据软件设计的最小授权(最小暴露)原则,将一些代码隐藏在函数作用域内部,不影响功能和实现,

但是可以将代码私有化,不过多的暴露变量或者函数。

而且可以避免命名重复的冲突。

⚠️为了避免全局变量污染和冲突,自定义的插件后者第三方库,最好定义自己的命名空间,将所有的变量和方法挂载到一个对象上。

如jquery。(单例模式)

缺点:

1)需要声明一个函数,可能导致污染全局作用域

2)需要调用声明的函数

解决方案:

//改善前:
var a= 2;
function foo() { // 函数声明,foo处于全局作用域
    var a= 3;
    console.log(a); // 3 
}
console.log(a); // 2
//改善后
var a= 2;
(function foo(){ // 不是以function开始,是函数表达式,作用域在foo内部
   var a = 3;
   console.log(a);//3
})();
console.log(a); // 2        
foo() // ReferenceError: foo is not defined

具名和匿名

匿名函数在栈追踪中没有有意义的函数名,会使得调试困难;不能引用自身;没有可读性;

所以建议在所有使用匿名函数的地方都改为具名函数

setTimeout(function time(){
    // 有名字的函数表达式
},100)

立即执行函数表达式IIFE(;是必要的)

(function IIFE(){
//...
})(params); // ;必要!第一个括号表示函数表达式,作用域被绑定在函数表达式自身的函数中,意味着IIFE只能在...的位置中被访问;第二个括号立即执行;
等同于
(function IIFE(){
}(params));

立即执行函数表达式可以传递参数,参数类型可以是变量,如window;也可以是函数。

// 传参是变量
var a = 2;
(function IIFE(global){
    var a = 3;
    console.log(a); //3
    console.log(global.a); //2
})(window); // 注意node环境中没有window;在console中可以测试

// 传参是函数 var a = 2; (function IIFE(def){ def(window); })(function def(global) { var a = 3; console.log(a); //3 console.log(global.a); //2 })

6. 块作用域

try/cacth

try/catch的catch分句中具有块作用域。

let

1)将变量绑定到块作用域

for(let i= 0; i< 10; i++){  
}
// 相当于将变量绑定到一个块作用域中
{
    let j;
    for(j =0; i<10;j++){
       let i=j;
    }
}    
console.log(i); // Uncaught ReferenceError: i is not defined

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

2)不能变量提升

{
    console.log(a); // ReferenceError
    let a = 2;
}

const

const也是块作用域变量,但是不可更改

7. 变量(声明)提升

原理: 变量和函数的声明提升是在代码被执行前(编译阶段)处理。提升阶段所有的代码处于待执行阶段!!不执行!!

var a = 2;
// js引擎将其分为两块:
var a;
//var a;定义声明在编译阶段进行
a = 2;
// 赋值声明等待执行阶段被执行

示例

1)变量声明提升

console.log(a);
var a = 2;
// 相当于
var a;
console.log(a);
a = 2;

2)函数声明提升

foo();
function foo() {
    console.log(a); // undefined
    var a= 2;
}
// 相当于
function foo() { // 函数声明提升;整个声明的函数都上移
   var a;   
   console.log(a); 
   a = 2;
}
foo();
⚠️ //var foo = function(){} 不是函数声明,是函数表达式。函数表达式的函数不会被提升。只有foo作为变量被提升!!!
foo(); // TypeError: foo is not a function
var foo = function(){}

⚠️函数重复声明的时候,后面的会覆盖前面的。而且因为函数声明提升,不管在任何位置访问,都以最后一个为主。

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

3)当同时存在函数声明和变量声明提升时,函数声明提升优先于变量声明提升;

⚠️!!!提升时不是将变量或者函数提升到整个程序的最上方!!!是当前作用域的最上方

1)变量提升
function foo() {
   var a;    // ⚠️变量提升到foo创建的作用域的顶部,不是全局作用域!!!
   console.log(a); 
   a = 2;
}
/*****在条件判断语句中变量声明提升和函数声明提升不同
变量声明在条件判断语句中会出现变量提升,而函数声明不会*******
*/ console.log(b, c);// undefined undefine var a = true; if (a) { var b = 1; } else { var c = 2; }
2)函数声明提升(条件判断)
foo(); // ❌UnCaught TypeError: foo is not a function
// js代码的规则是,如果有未捕捉异常,程序崩溃,且不再往下执行
var a = true;
if (a) {
    function foo() { 
        console.log('a');
    } // 函数声明在代码块内部,在编译阶段,声明提升只在块作用域中提升
} else {
    function foo() {
        console.log('b');
    }
}
/****************不抛出异常******************/
console.log(foo); 
var a = true;
if (a) { // 判断是在执行阶段进行判断的
    function foo() {
        console.log('a');
    }
} else {
    function foo() {
        console.log('b');
    }
}
/**运行结果如下***/
// undefined
// function foo() {       不抛出异常,代码正常执行,判断执行后,在全局
//    console.log('a');    声明函数
// }   
原文地址:https://www.cnblogs.com/lyraLee/p/11428325.html