作用域详解

1.作用域是什么?

作用域是一套根据名称在当前作用域及嵌套作用域中进行查找变量的规则。(在哪,怎么查)

用来存储变量(标识符)并之后可以方便的找到这些变量,确定当前代码对这些变量的访问权限。

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=2(隐式), b=..
// 4个RHS:foo, =a, a +, +b

3. 作用域嵌套

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

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

下图是个嵌套作用域。

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

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

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

作用域详解_第1张图片

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');    声明函数
// }   

你可能感兴趣的:(作用域详解)