2024-12-6 日报 Day27

2024-12-6 日报 Day27

Yuyang 前端小白🥬

今日的鸡汤

失望和希望,常常在同一条路上朝你走来。

今日学习内容

1、JS红皮书P87-94 第四章:变量、作用域与内存

今日笔记

1、typeof 操作符最适合用来判断一个变量是否为原始类型。更确切地说,它是判断一个变量是否为字符串、数值、布尔值或 undefined 的最好方式。如果值是对象或 null,那么 typeof 返回”object”,如下面的例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
let s = "Nicholas"; 
let b = true;
let i = 22;
let u;
let n = null;
let o = new Object();
console.log(typeof s); // string
console.log(typeof i); // number
console.log(typeof b); // boolean
console.log(typeof u); // undefined
console.log(typeof n); // object
console.log(typeof o); // object

typeof 虽然对原始值很有用,但它对引用值的用处不大。我们通常不关心一个值是不是对象,而是想知道它是什么类型的对象。为了解决这个问题,ECMAScript 提供了 instanceof 操作符,语法如下:
result = variable instanceof constructor
如果变量是给定引用类型(由其原型链决定,将在第 8 章详细介绍)的实例,则 instanceof 操作符返回 true。来看下面的例子:
console.log(person instanceof Object); // 变量 person 是 Object 吗?
console.log(colors instanceof Array); // 变量 colors 是 Array 吗?
console.log(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?
按照定义,所有引用值都是 Object 的实例,因此通过 instanceof 操作符检测任何引用值和Object 构造函数都会返回 true。类似地,如果用 instanceof 检测原始值,则始终会返回 false,因为原始值不是对象
注意 typeof 操作符在用于检测函数时也会返回”function”。当在 Safari(直到 Safari 5)和 Chrome(直到 Chrome 7)中用于检测正则表达式时,由于实现细节的原因,typeof也会返回”function”。ECMA-262 规定,任何实现内部[[Call]]方法的对象都应该在typeof 检测时返回”function”。因为上述浏览器中的正则表达式实现了这个方法,所以 typeof 对正则表达式也返回”function”。在 IE 和 Firefox 中,typeof 对正则表达式返回”object”。
2、变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。
全局上下文是最外层的上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的 window 对象(第 12 章会详细介绍),因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。
3、执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
 try/catch 语句的 catch 块
 with 语句
这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。看下面的例子:

1
2
3
4
5
6
7
function buildUrl() { 
let qs = "?debug=true";
with(location){
let url = href + qs;
}
return url;
}

这里,with 语句将 location 对象作为上下文,因此 location 会被添加到作用域链前端。buildUrl()函数中定义了一个变量 qs。当 with 语句中的代码引用变量 href 时,实际上引用的是location.href,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在 buildUrl()中的那个变量,它定义在函数上下文的变量对象上。而在 with 语句中使用 var 声明的变量 url 会成为函数上下文的一部分,可以作为函数的值被返回;但像这里使用 let 声明的变量 url,因为被限制在块级作用域(稍后介绍),所以在 with 块之外没有定义。

注意 IE 的实现在 IE8 之前是有偏差的,即它们会将 catch 语句中捕获的错误添加到执行上下文的变量对象上,而不是 catch 语句的变量对象上,导致在 catch 块外部都可以访问到错误。IE9 纠正了这个问题。
4、在使用var作为函数作用域声明时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文。在 with 语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文,如下面的例子所示:

1
2
3
4
5
6
function add(num1, num2) { 
var sum = num1 + num2;
return sum;
}
var result = add(10, 20); // 30
console.log(sum); // 报错:sum在这里不是有效变量

在这个例子中,sum 是 add()函数的局部变量,只能在函数内部访问。如果在函数外部访问 sum,就会导致错误。这是因为 sum 是在函数上下文中定义的,而不是在全局上下文中定义的。如果在函数内部没有使用 var 声明 sum,那么 sum 就会被自动添加到全局上下文中,如下面的例子所示:

1
2
3
4
5
6
function add(num1, num2) { 
sum = num1 + num2;
return sum;
}
var result = add(10, 20); // 30
console.log(sum); // 30

在这个例子中,sum 是全局变量,因为它是在函数内部而没有使用 var 声明的。这种情况下,sum 会被添加到全局上下文中,因此在函数外部也可以访问到它。这种行为在严格模式下会导致错误,因为在严格模式下不能给未声明的变量赋值。
var声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫做变量提升(hoisting)。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。可是在实践中,提升也会导致合法却奇怪的现象,即在变量声明之前使用变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = "Jake";

// 等价于:
name = "Jake";
var name;

function fn1(){
var name = "Jake";
}
// 等价于:
function fn1(){
var name;
name = "Jake";
}

通过在声明之前打印变量,可以验证变量会被提升。声明的提升意味着会输出 undefined 而不是Reference Error:

1
2
3
4
5
6
console.log(name); // undefined 
var name = 'Jake';
function() {
console.log(name); // undefined
var name = 'Jake';
}

5、let的作用域是块级的,其由最近的一对包含花括号{}界定。换句话说,if块、while块、function块,甚至连单独的块也是let声明变量的作用域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (true) {
let a;
}
console.log(a); // ReferenceError: a is not defined

while(true) {
let b;
}
console.log(b); // ReferenceError: b is not defined

function foo(){
let c;
}
console.log(c); // ReferenceError: c is not defined

{
let d;
}
console.log(d); // ReferenceError: d is not defined

let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重复的 let 声明会抛出 SyntaxError。

1
2
3
4
5
6
var a = 1;
var a = 2;
console.log(a); // 2

let b = 1;
let b = 2; // SyntaxError: Identifier 'b' has already been declared

let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情况应该避免。来看下面两个例子:

1
2
3
4
for (var i = 0; i < 10; ++i) {} 
console.log(i); // 10
for (let j = 0; j < 10; ++j) {}
console.log(j); // ReferenceError: j 没有定义

严格来讲,let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var是不一样的。

这是一个很容易产生误解但非常关键的 JavaScript 知识点,我们来详细讲讲:

🌟 为什么说 let 会被“提升”,但不能用?

✅ 提升的定义

“变量提升(Hoisting)” 是指:

在 JavaScript 的编译阶段,变量的声明会被“提前”到当前作用域的顶部。

所以:let 声明的变量,其实也被提升了!
• 但不同于 var 被提升后初始化为 undefined,
• let(和 const)在提升后不会立刻初始化,
• 而是在真正的声明语句执行前处于“暂时性死区”(TDZ)中,
• 只要在 TDZ 里访问这个变量,就会报错!

🧠 什么是“暂时性死区(TDZ)”?

TDZ 是从变量绑定开始,到代码执行到声明语句的那一刻之间的这段时间。

在这段时间内,访问变量会抛出 ReferenceError,因为它还没有被初始化。

示例一:

console.log(a); // ❌ ReferenceError: Cannot access ‘a’ before initialization
let a = 10;

尽管 a 被“提升”了,但在 let a = 10 执行之前,你就是不能访问它!

🔁 和 var 的区别

console.log(b); // ✅ undefined(不会报错)
var b = 20;

var 的声明也被提升了,并且提前赋值为 undefined,所以访问不会报错。

✅ 小总结:

特性 var let / const
是否提升 是 是
提升后是否初始化 初始化为 undefined 不初始化,进入 TDZ
提升后能否访问 能(值为 undefined) 不能,访问会报 ReferenceError
是否有暂时性死区(TDZ) ❌ 没有 ✅ 有,直到真正的声明位置才可用
6、除了 let,ES6 同时还增加了 const 关键字。使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const a; // SyntaxError: 常量声明时没有初始化
const b = 3;
console.log(b); // 3
b = 4; // TypeError: 给常量赋值
const 除了要遵循以上规则,其他方面与 let 声明是一样的:
if (true) {
const a = 0;
}
console.log(a); // ReferenceError: a 没有定义
while (true) {
const b = 1;
}
console.log(b); // ReferenceError: b 没有定义
function foo() {
const c = 2;
}
console.log(c); // ReferenceError: c 没有定义
{
const d = 3;
}
console.log(d); // ReferenceError: d 没有定义

const 声明只应用到顶级原语或者对象。换句话说,赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制。

1
2
3
4
5
const o1 = {}; 
o1 = {}; // TypeError: 给常量赋值
const o2 = {};
o2.name = 'Jake';
console.log(o2.name); // 'Jake'

如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败:

1
2
3
const o3 = Object.freeze({}); 
o3.name = 'Jake';
console.log(o3.name); // undefined

由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。
7、标识符查找:当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。(注意,作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未声明。

此页目录
2024-12-6 日报 Day27