js三座大山之函数-静态词法作用域

273 阅读6分钟

js三座大山

一:函数式编程
js三座大山之函数1 
js三座大山之函数-静态词法作用域
js三座大山之函数-运行时this的四种指向

二:面向对象编程
js三座大山之对象,继承,类,原型链

三:异步编程:
js三座大山之异步一单线程,event loope,宏任务&微任务
js三座大山之异步二异步方案
js三座大山之异步三promise本质
js三座大山之异步四-Promise的同步调用消除异步的传染性
js三座大山之异步五基于异步的js性能优化
js三座大山之异步六实现微任务的N种方式
js三座大山之异步七实现宏任务的N种方式

静态词法作用域作用域与运行时this

完整的描述应该为:变量的静态词法作用域与函数的运行时动态this。
有很多同学会将变量的作用域和函数的运行时this指向搞混淆其实这可以说是两个东西。作用域是修饰变量的。 例如function fn fn即为指向函数的变量,作用域是指能够访问到这个fn的范围,这与代码的编写位置和声明方式有关,而this是指谁调用的fn例如obj.fn(),this就是函数的调用者。

静态词法作用域:那么这句话到底该如何理解呢?

静态即代码书写的位置
词法即通过关键字声明的语法变量, 例如通过var(不推荐) let const class function import export等
作用域是代码中定义变量的区域,它决定了变量的可见性和生命周期,作用域层层嵌套是一个树状结构从叶子到根节点依次查询访问。
所以可以说静态词法作用域是JavaScript中确定变量访问权限的一种机制。在这种机制下,一个变量的作用域在定义时就已经确定了,而不是在函数调用或者运行时才确定的。

所以可以得出公式:关键字 + 书写位置 => 作用域

全局作用域: 通过var/function + 全局作用下生成的变量

在代码的任何地方都可以访问的变量就是定义在全局作用域中。在浏览器环境中,全局作用域就是window对象。

<script>
var globalVar = 'I am global'; // 定义在全局作用域中
console.log(globalVar); // 输出:"I am global"
</script>

模块作用域:通过import/expport + js文件。

在ES6模块中,每个模块都有自己的作用域。在一个模块中定义的变量,除非显式地导出,否则其他模块不能访问。

// module1.js
const moduleSelf = '一个内部变量' //没有被导出所以其他模块不能访问到 只能在本文件内部访问到
const moduleVar = 'I am module scoped';
export default moduleVar;

// module2.js
import moduleVar from './module1.js';
console.log(moduleVar); // 输出:"I am module scoped"

函数作用域:通过function + 任何位置

在函数内部定义的变量只能在该函数内部访问,这就是函数作用域。

function myFunction() {
  var functionVar = 'I am local';
  console.log(functionVar); // 输出:"I am local"
}
myFunction();
console.log(functionVar); // 报错:Uncaught ReferenceError: functionVar is not defined

块级作用域:通过let/const/class + 在{}内部

在ES6中,使用let,const, class关键字定义的变量具有块级作用域,也就是说,它们只在定义它们的代码块中可见。

if (true) {
  let blockVar = 'I am block scoped';
  console.log(blockVar); // 输出:"I am block scoped"
}
console.log(blockVar); // 报错:Uncaught ReferenceError: blockVar is not defined

if (true) {
  class P{}
  console.log(P); // 输出:class P{}
}
console.log(P); // Uncaught ReferenceError: P is not defined

截屏2023-12-12 下午4.09.55.png

变量提升Hoisting&暂时性死区TDZ

- Hoisting:
  • var声明的变量都会被提升
    这意味着你可以在声明变量或函数之前使用它们,而不会引发错误。这是因为JavaScript引擎在解析代码时,会先读取所有的变量,并将它们提升到它们所在的作用域的顶部。需要注意的是,只有声明会被提升,初始化(即赋值操作)不会被提升
  • 函数提升
    只有函数声明的方式才会被提升,提升的效果与变量类似,但是有所不同,函数可以看做是声明和初始化都会被提升但是根据函数书写的位置会有不同表现。
    看个例子:
console.log(myVar); // 输出:undefined
var myVar = 5;
console.log(myVar); // 输出:5

解析后:
var myVar;
console.log(myVar); // 输出:undefined
myVar = 5;
console.log(myVar); // 输出:5


console.log(fn) // 输出 function fn(){}
function fn(){}
console.log(fn) // 输出 function fn(){}

解析后:
var fn = function(){} // 可以看做是这样 实际不是
console.log(fn) // 输出 function(){}
console.log(fn) // 输出 function(){}

在看个例子:

if('name' === 123){
    function a(){}
    var b = 456;
    let c = 10
};
console.log('test a', a); // test a undefined
console.log('test b', b); // test b undefined
console.log('test c', c); // ReferenceError: c is not defined

1:a是一个函数方式声明的应该被提升到作用域顶部,而if(控制语句)没有自己的作用域,所以a在外层应该是可以访问到并正确打印函数的,但是这里有一个例外。实际上在控制语句if for while等会将函数按照表达式解析。

类似于下面的代码:
if('name' === 123){
    var a = function(){}
    var b = 456;
    let c = 10
};

然后在这样
var a;
var b;
if('name' === 123){
    a = function(){}
    b = 456;
    let c = 10
};

所以得到的结果function a和var b是一样的打印。
这是一个容易引起混淆的地方,因为函数声明在控制流语句中的行为与它们在其他地方的行为不同。为了避免这种混淆,最佳实践是避免在控制流语句中声明函数,而是在函数外部声明,然后在需要的地方调用。
2:c是使用let声明的 是块级别作用域 在块外部无法访问到 所以打印是报错的。

再来一个例子:

var foo = 'hello';
(function(foo){
  console.log(foo);
  var foo = foo || 'world';
  console.log(foo);
})(foo);
console.log(foo);


解析后
var foo;
foo = hollo;

(function(){ // 函数形成自己的作用域 里面的变量对外不可见
  var foo = 'hello' // 函数参数编译后可以这样理解
  console.log(foo);
  foo = foo || 'world'; // 因为已经存在foo变量了所以不在重复声明 这里变为赋值语句
  console.log(foo);
})(foo);

console.log(foo); // 这里读取的是外层作用域的foo

输出 hello hello hello
- TDZ:

当你使用let,const,class关键字声明变量时,这些变量会被提升到它们所在的作用域的顶部,这就是变量提升。但是,与var不同,声明的变量在声明之前是不可访问的。如果你试图在声明之前访问这些变量,JavaScript会抛出一个错误。这个从块的开始到变量声明位置之间的区域,就被称为“暂时性死区”(Temporal Dead Zone,TDZ)。

console.log('test age', age) // ReferenceError: Cannot access 'age' before initialization
let age = 19;

console.log('test P', P) //ReferenceError: Cannot access 'P' before initialization
class P{};


console.log('test X', X) // ReferenceError: X is not defined

之所有要有TDZ就是要让开发者时刻保持先声明在使用的良好规范,以免大家放飞自我,bug满天飞。