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