作用域(scope)
作用域分为静态作用域(即词法作用域)和动态作用域。
作用域统称就是一套存储并查找变量的规则
1. 动态作用域
动态作用域,是调用时才确定
php
//scope.bash文件,执行命令bash ./scope.bash
a =1
function foo(){
echo $a;
}
function bar(){
local a=2;
foo;
}
bar//2
2. 静态作用域(词法作用域)
静态作用域,是声明时就确定 (就近原则 )了,JS是静态作用域,
scss
//静态作用域
var a = 1;
function foo(){
console.log(a)
}
function bar(){
var a =2;
foo()
}
bar()//1
作用域分为全局作用域,局部作用域,变量作用域
2.1. 全局作用域
最外层的区域,尽量少声明全局变量,容易造成全局变量污染
2.2. 局部作用域
2.2.1. 函数作用域
1.包装函数
在任意代码片段外部添加包装函数,形成函数作用域,外部作用域无法访问包装函数内部的任何内容。
但包装函数名称本身"污染"了所在区域,并且必须显示的调用才能运行代码
javascript
var a = 2;
function foo() { // <-- 添加这一行,包装函数
var a = 3;
console.log(a); // 3
} // <-- 以及这一行
foo(); // <-- 以及这一行,显示调用
console.log(a); // 2
解决办法:立即执行函数 ****(function(){})()
2.立即执行函数
立即执行函数是一个函数表达式
立即执行函数会被当作函数表达式而不是一个标准的函数声明来处理
a.判断是函数声明还是函数表达式?
区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。 如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
b.立即执行函数表达式的几种用法
javascript
1.(function(){})(),常见:匿名,值得实践:具名
2.(function (){}())改进版,与1一致,用哪个看个人喜好
用法:
1.如上立即执行,防止变量污染
2.传递参数(function(形参){})(实参)
3.解决undefined被赋值的错误
(function(undefined){})()不常见
4.倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去。这种模式在UMD(Universal Module Definition通用模块定义)项目中被广泛使用。尽管这种模式略显冗长,但有些人认为它更易理解
var a=2
(function IIFE(def){
def(window)
})(function def(global){
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
})
UMD是通用模块定义,是一种跨平台、支持多种编程语言的软件开发模型和框架。
UMD模式下常用
解决 js 各种模块化兼容问题;提供一个通用的输出格式?
回调函数?
函数式编程?
c.匿名函数缺点
1.难调试
2.不能被调用(调用还需有函数名),递归用arguments.callee调用自身,但arguments.callee有性能问题,调用自身
3.影响可读性
2.2.2. 块作用域
1.{},let和const可产生块作用域
{},let和const可产生块作用域,var不会产生块作用域
只要声明是有效的,在声明中的任意位置都可以使用{ .. }括号来为let创建一个用于绑定的块
let没有变量提升,在当前作用域也没有提升,写在哪就是在哪,只对当时作用域后面的代码有效
let循环
for循环头部的let不仅将i绑定到了for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
css
for (let i=0; i<10; i++) {
console.log(i);
}
console.log(i); // ReferenceError
const常量
ini
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在if中的块作用域常量
a = 3; // 正常!
b = 4; // 错误!
}
console.log(a); // 3
console.log(b); // ReferenceError!
2.废弃的with是块作用域
3.catch生成的是块作用域(try/catch)
静态检查工具不知道catch有块作用域,因为所有变量都被安全地限制在块作用域内部,但是静态检查工具还是会很烦人地发出警告。
2.2.2.1. 块作用域的用途
1.垃圾回收
javascript
function process(data) {
// 在这里做点有趣的事情
}
var someReallyBigData = { ..};
process(someReallyBigData);
var btn = document.getElementById("my button");
btn.addEventListener("click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase=*/false );
click函数的点击回调并不需要someReallyBigData变量。理论上这意味着当process(..)执行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于click函数形成了一个覆盖整个作用域的闭包,JavaScript引擎极有可能依然保存着这个结构(取决于具体实现)。
块作用域可以打消这种顾虑,可以让引擎清楚地知道没有必要继续保存someReallyBigData了
javascript
function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容完事可以销毁!
{
let someReallyBigData = { .. };
process(someReallyBigData);
}
var btn = document.getElementById("my button");
btn.addEventListener("click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
2.显式的块,方便代码重构,可以转移到任何位置
ini
var foo = true;
if (foo) {//隐式的块
{ // <-- 显式的块,方便代码重构,可以转移到任何位置
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
}
console.log(bar); //ReferenceError
2.3. 变量作用域
包含:全局变量,局部变量,块变量(var没有块变量)
3. eval和with(已废弃)
除了eval和with(生成动态作用域,已废弃,不建议用,欺骗词法作用域会导致性能下降。)
eval
JavaScript中的eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。
eval(..)通常被用来执行动态创建的代码,因为像例子中这样动态地执行一段固定字符所组成的代码,并没有比直接将代码写在那里更有好处。
eval严格模式下,不能修改别的作用域
JavaScript中还有其他一些功能效果和eval(..)很相似。setTimeout(..)和setInterval(..)的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的函数代码。这些功能已经过时且并不被提倡。不要使用它们!new Function(..)函数的行为也很类似,最后一个参数可以接受代码字符串,并将其转化为动态生成的函数(前面的参数是这个新生成的函数的形参)。这种构建函数的语法比eval(..)略微安全一些,但也要尽量避免使用。
with
ini
function foo(obj) {
with (obj) {a = 2;}
}
var o1 = {a: 3 };
var o2 = {b: 3};
foo(o1);
console.log(o1.a); // 2
foo(o2);
console.log(o2.a); // undefined
console.log(a); // 2------不好,a被泄漏
with生成一个完全隔离的词法作用域o1或o2,a=2执行LHS查询,在写with作用域中找不到,最后找到全局作用域(with所在函数作用域),依然没找到,于是创建了全局变量a,造成变量污染
with本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。
eval和with缺点
另外一个不推荐使用eval(..)和with的原因是会被严格模式所影响(限制)。with被完全禁止,而在保留核心功能的前提下,间接或非安全地使用eval(..)也被禁止了。
最悲观的情况是如果出现了eval(..)或with,所有的优化可能都是无意义的,因此最简单的做法就是完全不做任何优化。
如果代码中大量使用eval(..)或with,那么运行起来一定会变得非常慢
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化
4. 最小授权或最小暴露原则
在软件设计中,应最小限度地暴露必要内容,而将其他内容都"隐藏"起来,比如某个模块或对象的API设计。
4.1. 全局命名空间
这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象 。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中
4.2. 模块管理
从众多 模块管理器中挑选一个来使用 。使用这些工具,任何库都无需将标识符加入到全局作用域中 ,而是通过 依赖管理器 ( npm vite component maven等都是 ) 的机制将库的标识符显式地导入到另外一个特定的作用域中。
参考
- 《你不知道的JavaScript》
最后
决定开始写博客笔记,这是JavaScript系列的第二篇,会持续更新,您对我的关注、点赞和收藏,是对我最大的支持!欢迎关注、评论、讨论和指正!