本文针对 JavaScript 初学者,深入解析连等赋值的变量泄露风险、自增运算符的执行细节、包装对象的生命周期以及引用类型传递机制。通过引擎执行流程剖析,帮助开发者避开常见陷阱,提升代码健壮性。
JavaScript 基础避坑指南:连等、自增与引用类型详解
本文适合 JavaScript 初学者,或已学习前端一段时间但对 JS 核心概念尚不清晰的开发者阅读。
学习目的
本文旨在帮助 JavaScript 基础薄弱的同学深化对语言机制的理解。
本文将针对初学者容易遇到的 JavaScript(以及多数编程语言共有的)常见陷阱进行讲解。
具体讲解内容如下:
1. 连等赋值
2. 自增运算符(i++)
3. 包装对象机制
4. 引用类型传递
5. 逻辑与(&&)和逻辑或(||)运算符
详细讲解部分
连等赋值的陷阱与变量泄露
1. 连等赋值
小试牛刀
连等是常见的表达式写法,但并非所有场景都适用。它仅对字面量安全,对引用类型则可能导致不可预期的结果。
// 字面量连等得到想要的结果
var a,b;
a = b = 2;
a // 2
b // 2
// 引用类型连等不可预测
var arr1, arr2;
arr1 = arr2 = []
arr1[0] = 10
arr2[0] // 10
//引用类型连等使得两个引用指向一个对象,操作其中一个,两个值都变
上述代码展示了常见的连等用法。有时我们需要将多个变量初始化为同一值,这种写法很方便。然而,如果是引用类型,直接连等赋值可能会引发问题。
此外,连等赋值存在一个隐蔽的漏洞:未声明的变量可能会泄露到全局作用域。虽然前面的例子没有体现,但请看下面的代码:
function fn (num) {
var a = b = num;
a // num
b // num
}
fn(10)
a // 报错
b // 10
// 我们并不没有定义全局变量b
执行 fn 函数后,全局作用域中出现了变量 b。这是为什么?分析语句 `var a = b = num`,可以将其拆解为两个步骤来看。
var a
a = b = num
//只声明了a
实际上,我们只显式声明了变量 a。在连等中,变量 b 并未被声明,因此它被隐式地挂载到了全局 window 对象上,导致变量泄露至全局。
初出茅庐
上述仅为简单示例,接下来我们看一个更复杂的例子。
var a = {x: 1}
var b = a
a.x = a = {y: 1}
a.x // undefined
b.x // {y: 1}
这个例子源自一道测试题,乍看复杂,实则逻辑清晰。
1. 变量 a 和 b 引用了同一个对象 `{x: 1}`。
2. `a.x` 引用了原对象的 x 属性,而 a 本身是一个引用变量。
3. `a = {y: 1}` 仅将引用变量 a 的指针指向了另一个新对象 `{y: 1}`。
4. `a.x = a`:左侧的 `a.x` 仍然指向原对象的 x 属性(即 b 所引用的对象的 x 属性)。
5. 赋值操作完成。
如果上述解释仍未让你完全理解,别急,下面我们将深入 JavaScript 引擎的工作机制,帮你彻底搞懂。
庖丁解牛
引擎工作原理:引擎在解析 JavaScript 表达式时,会进行 LHS(左值,即赋值目标)和 RHS(右值,即查找源)查询(详见《你不知道的 JavaScript》)。我们可以简单理解为:LHS 涉及赋值,RHS 涉及查找。
下面,我们结合上述例子,演示引擎的具体工作流程。
var a = {x: 1}
// 引擎:我将要对a变量LHS(赋值),内容是{x: 1}
// 作用域: 刚声明了a变量,给你。
var b = a
// 引擎: 我将要对a变量RHS(查找)
// 作用域: 你刚刚给它LHS了,给你吧
// 引擎: 我将要对b变量LHS(赋值),内容为a变量指向的对象
// 作用域:刚声明了b变量,给你。
a.x = a = {y: 1}
// 引擎:我将要对a进行LHS(赋值),内容是另一个对象{y:1}
// 作用域:可以,给你,但好像还有其他命令,先不要赋值。
// 引擎: 是的,下一步我还需要对a.x 进行LHS(赋值),内容是将要改变的a变量
// 作用域: 可以,a变量指向的对象有x属性,不过马上a就改变了,不过没关系,b变量也指向那个对象,赋值完后,你可以用b变量引用旧对象。
// 引擎:了解了,我先把a变量赋值为一个新的对象,然后把原来的a变量指向的对象的x属性赋值为 新a。
a.x // undefined
// 引擎: 我需要拿到a变量指向的对象的x属性
// 作用域: 你刚刚改变了a的指向,现在的a指向的对象已经没有x属性了
b.x // {y: 1}
// 引擎: 我需要拿到b变量指向的对象的x属性
// 作用域: 你是想拿到你旧对象的x属性吧,给你,不过已经被你在之前改变了值,现在b.x的值就是a指向的新对象的值。
以上为个人理解,非官方权威认证。若想深入了解执行过程,建议参考《你不知道的 JavaScript》。若本节内容有误,欢迎指正。
自增运算符的执行细节
2. 自增运算符(++)
大家最常用 ++ 操作符,其实它并无奇特之处,但对于新手而言,是否真正了解其内部机制?
var a = 1;
var b = a++
a // 2
b // 1
var c = 1;
var d = ++ c;
c // 2
d // 2
前自增(++a)和后自增(a++)的区别在于:前者返回自增后的表达式值,后者返回自增前的值。我们可以将两者分解,观察其执行过程。
b = a++
// 等价于 ...
b = a
a = a + 1
//.........................
b = ++ a
// 等价于 ...
a = a + 1
b = a
这主要是一个运算顺序问题,相对容易理解。但这里有一个常见的陷阱,如下所示。
前几天有人提问:`1++` 等于几?有人回答:2。
很多人第一反应也是 2,但这完全错误!`1++` 会报错,因为它不是合法的表达式。原因如下:
1 ++
// 等价于
1 = 1 + 1
// 引擎对 1 进行LHS(赋值),作用域发现他是非法变量,所以会报错 左值无效。
包装对象的生命周期
3. 包装对象机制
当我们使用字符串获取长度或调用截取方法时,是否想过:字面值只是一个值,为什么它会有方法和属性?按理说只有对象才具备这些。确实如此,但在执行表达式时,JavaScript 会自动生成包装对象。如果你已了解此知识点,可跳过本节。
var str = 'hello'
str.length // 5
str.aaa = 5
str.aaa // undefined
我们定义字符串 str 并获取其长度为 5,但尝试手动添加属性 aaa 时却获取不到。这需要用包装对象的生命周期来解释:包装对象的声明周期仅存在于单个表达式执行期间。
var str = 'hello'
str.length
// 等价于
new String(str).length
str.aaa = 5
//等价于
new String(str).aaa = 5
str.aaa
// 等价于
new String(str).aaa
也就是说,每次访问 str 的属性时,JavaScript 都会先将其包装为 String 对象,操作完成后立即释放该对象。因此,上述两次 `str.aaa` 访问涉及的是不同的临时对象,第二次获取 aaa 自然为 undefined。若不了解,可搜索“JS 包装对象”获取详细解答。
引用类型传递机制
4. 引用类型传递
大多数语言都支持引用类型,即对象变量。在 C 语言中,我们将引用类型理解为指针,它动态指向一块内存,随代码变化而改变指向。JavaScript 同理。
在编写代码时,务必牢记引用类型变量与字面值变量的区别,它们各有不同的适用场景。
var global = new Object()
function setColor (obj) {
obj.color = 'blue'
obj = new Object()
obj.color = 'red'
}
setColor(global)
global.color // blue
这是《JavaScript 高级程序设计》中的一个经典例子:我们将对象 global 传递给 setColor 函数,在函数内部进行了操作,打印出 `global.color` 为 'blue',而非 'red'。这正是引用类型传递的结果。
1. global 变量是引用类型,它指向一个对象。
2. 传递给 setColor 函数后,参数 obj 引用了 global 所指向的对象(下文称为 globalObj)。
3. 给 globalObj 赋值 color 属性为 'blue' 字符串,此时 `global.color` 变为 'blue'。
4. 将 obj 指向另一个新对象 localObj,使得 obj 与 global 的原始连接断开。
5. 将 localObj.color 赋值为 'red'。
可以看出,我们并未对 global 对象的 color 赋值为 'red','red' 被赋给了另一个新对象的 color 属性。
结论:引用类型传递是将两个变量指向同一个对象,而字面量的传递仅仅是值的复制。我们可以将引用类型传递到函数中进行修改,但不能在函数内部改变传递进来的字面值(因为改变的是局部副本的指向)。
逻辑运算符的返回值特性
5. 逻辑与(&&)和逻辑或(||)
两者的基本用法想必大家都已了解,大部分用于 if 判断,例如:
var a = 2;
var b = false
if (a && b) {
alert('best')
}
if (a || b) {
alret('good') // 运行
}
它们的用法不仅限于判断,还可以用来提升代码的健壮性。
var obj = {}
if (obj.info.user === 'xu') { // terrible
// ..
}
if (obj.info && obj.info.user === 'xu' ) { // good
// ...
}
如果直接判断 `obj.info.user` 可能会因属性不存在而报错,导致程序终止;而采用下面的判断方式则能显著提高代码健壮性,避免轻易崩溃。
重点:&& 和 || 运算符并不一定返回 true 或 false,它们会返回终止该表达式求值的那个操作数的值。
true && 6 // 6
NaN && false // NaN
'0' || 6 // '0'
false || true // true
false || 0 && 1 // 0
false || 1 && 0 // 0
&& 和 || 中,&& 的优先级高于 ||。
&& 操作符:如果左边为假,返回左边值;否则,始终返回右边值。
|| 操作符:如果左边为真,返回左边值;否则,始终返回右边值。
结尾
JavaScript 基础章节的简要介绍到此结束,内容并不全面,敬请谅解。如有错误,欢迎指正。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持本站!