深入解析JavaScript中13个鲜为人知的特性与陷阱

作者:纸飞机

时间:2026-10-23

浏览:140

本文深入探讨了JavaScript中13个常被忽视的特性,包括逗号运算符的链式求值、in关键字的属性检查、Array构造函数在单整数参数时的内存分配行为,以及Function构造函数的动态创建机制。此外,还详细解析了数组与对象的解构差异、利用length属性截断数组、arguments对象的callee属性、跳过实例化括号、void运算符确保undefined、__proto__原型继承以及一元运算符和指数运算符的具体用法与潜在陷阱。

文章目录

重要

本文所述的大部分功能在现代JavaScript环境中已被暂停使用或标记为不推荐。尽管它们仍频繁出现在各类技术书籍中,但理解其原理对于深入掌握语言底层机制依然具有学习价值。

逗号运算符与 in 关键字

一、逗号运算符

逗号运算符(,)用于分隔多个表达式,并返回链式中最后一个表达式的计算结果。


let oo = (1, 2, 3)
console.log(oo) // 3

考虑包含表达式 1、2 和 3 的情况。所有表达式均会被依次求值,最终将最后一个表达式的值赋给变量 oo。

在 for 循环中,这一特性常被用于简化逻辑:


for(let i = 0, ii = 1; i< 10; i++, ii--) { ... }

在编写简短的箭头函数(lambda)时,逗号运算符同样适用:


const lb = (a, b, arr) => (arr.push(a*b), a*b)

上述代码包含两个语句:第一个将乘法结果推入数组 arr,第二个将乘数 a 和 b 推入数组。最终返回的是第二个语句的结果,即被调用者接收到的值。

对于三元运算符,逗号运算符也颇具价值,因为与简短 lambda 语法类似,它仅接受表达式而非语句。

二、in 关键字

in 关键字用于检查对象中是否存在指定属性。虽然我们在 for..in 循环中经常使用它,但可能未意识到 in 本身也是一个独立的关键字。


const o = {
  prop: 1
}
console.log("prop" in o) // true

可以看到,in 可以脱离 for..in 循环独立使用。

它检查 "prop" 是否作为属性存在于对象 o 中。由于我们在 o 中定义了 "prop" 属性,因此返回 true。

若检查一个未定义的属性:


const o = {
  prop: 1
}
console.log("prop1" in o) // false

此时返回 false,因为 "prop1" 在对象 o 中并未定义。

Array 与 Function 构造函数

三、Array 构造函数

你是否知道,可以不使用传统字面量方法来定义数组?


const arr = [1, 2, 3]

具体如何实现?

我们可以使用 Array 构造函数:


const arr = new Array(1, 2, 3)

传递给 Array 构造函数的参数排列将构成其索引的基础。

参数 1 是第一个参数,其索引为 0;参数 2 是第二个参数,其索引为 1;参数 3 是第三个参数,其索引为 2。


arr[0] // 1
arr[1] // 2
arr[2] // 3

因此,


const arr = new Array(1, 2, 3)

与


const arr = [1, 2, 3]

表达的含义相同。

然而,使用 new Array() 存在一个特殊问题,例如:


var a = new Array(10, 20);
a[0] // 返回 10
a.length // 返回 2

但:


var a = new Array(10);
a[0] // 返回 undefined
a.length // 返回 10

这种情况仅当向 Array 构造函数传递单个整数(大于等于 0 的整数,否则报错)时发生。这是为什么呢?

实际上,新的 Array 构造函数借鉴了某些编程语言的思路,在这些语言中,需要为数组指定内存大小,从而避免 ArrayIndexOutOfBounds 异常。


int *a = (int *) malloc( 10*sizeof(int) ); // ya ol' c
int *a = new int[10]; // c++
int[] a = new int[10]; // java

是的,它实际上是在创建一个长度为 10 的数组。虽然 JavaScript 中没有 sizeof 函数,但 toString 方法足以证明这一点。


a.toString() // 返回 ",,,,,,,,," 它相当于 [,,,,,,,,,]
a // [empty × 10]

因此,当向 new Array 传递一个参数时,JavaScript 引擎会为该参数大小的数组分配空间。

这也体现在 EcmaScript 规范中:

逗号运算符与 in 关键字

看,这并不矛盾。规范中包含了所有相关描述。在得出任何结论之前,我们应始终先阅读任何语言的官方规范。

四、Function 构造函数

你是否知道,可以使用 Function 构造函数来定义函数。

如果不理解,让我解释得更清楚些。在 JavaScript 中,我们通常这样定义函数:


const mul = (a, b) => a * b

// 或
function mul(a, b) {
  return a * b
}

// 或
const mul = function(a, b) {
  return a * b
}

我们也可以这样做,以实现相同的功能:


const mul = new Function("a", "b", "return a * b")

传递给 Function 的参数构成了函数的参数列表和函数体。变量 mul 成为函数名称。

并且,最后一个参数将成为函数的主体,而最后一个参数之前的参数将成为函数的参数。

在 mul 函数中,"a" 和 "b" 是函数接收的参数,"return a * b" 是函数主体。它实现了将 "a" 和 "b" 相乘并返回结果的功能。

我们使用 mul(...) 调用该函数,并传入参数:


const mul = new Function("a", "b", "return a * b")

console.log(mul(7, 8)) // 56

根据 MDN 文档:

Function 构造函数创建一个新的 Function 对象。直接调用此构造函数可用动态创建函数,但会遭遇来自 eval 的安全问题和相对较小的性能问题。然而,与 eval 不同的是,Function 构造函数只在全局作用域中运行。

数组解构与 length 属性操作

五、数组解构

我们可以通过使用元素的索引号来分解数组中的元素。


const arr = [1, 2, 3]

元素 1、2、3 的索引分别为 0、1、2,即:


arr[0] // 1

在日常开发中,我们最常使用的是对象解构:


let o = {
  prop: 1
}
o["prop"] // 1

// 解构
const {prop} = o
prop // 1

因此,我们将解构用于数组上:


const arr = [1, 2, 3]
const { 0: firstA, 1: secA, 2: thirdA } = arr

firstA // 1
secA // 2
thirdA // 3

所以我们可以使用索引号来提取元素。索引是定义数组中元素位置的属性。


const arr = [1, 2, 3]

这相当于:


const arr = {
  0: 1,
  1: 2,
  2: 3,
  length: 3
}

数组也是对象,这就是为什么要对其进行对象分解的原因,但是还有一种特殊的数组解构语法:


const [first, second, third] = arr

first // 1
second // 2
third // 3

注意:应尽可能避免依赖数组中特定位置的信息(如开始、结束索引是什么)。

六、使用 length 属性减少数组内容

数组中的 length 属性表示数组中元素的数目。


const arr = [1, 2, 3]
arr.length // 3

减小 length 属性值,会使 JavaScript 引擎将数组元素个数减少到与 length 属性的值相等。


const arr = [1, 2, 3]
arr.length // 3
arr.length = 1
arr // [1]

arr 的 length 属性值更改为 1,因此 arr 减少了元素个数,使其等于 length 属性值。

如果增加 length 属性,则 JavaScript 引擎将添加元素(未定义的元素)以使数组中的元素数量达到 length 属性的值。


const arr = [1, 2, 3]
arr.length // 3
arr.length = 1
arr // [1]

arr.length = 5
arr // [1, empty × 4]

arr 中原本只有一个元素,然后我们将长度增加到 5,因此又增加了 4 个元素长度,使元素数达到 5。

Arguments 对象与实例化简化

七、Arguments 对象

我们可以使用 arguments 对象获取传递给函数的参数,而无需在函数中明确定义 arguments 变量:


function myFunc() {
  console.log(arguments[0]) // 34
  console.log(arguments[1]) // 89
}

myFunc(34,89)

arguments 对象是数组索引的。也就是说,属性是数字,因此可以通过键引用进行访问。

arguments 对象是从 Arguments 类实例化的,该类具有一些很酷的属性。

arguments.callee.name 指当前正在调用的函数的名称。


function myFunc() {
  console.log(arguments.callee.name) // myFunc
}

myFunc(34, 89)

arguments.callee.caller.name 是指调用当前执行函数的函数的名称。


function myFunc() {
  console.log(arguments.callee.name) // myFunc
  console.log(arguments.callee.caller.name) // myFuncCallee
}

(function myFuncCallee() {
  myFunc(34, 89)
})()

这在可变参数功能中特别有用。

八、跳过 ()

你是否知道,在实例化对象时可以跳过方括号 ()?

例如:


class D {
  logger() {
    console.log("D")
  }
}

// 一般情况下,我们这么做:
(new D()).logger(); // D

// 其实,我们可以跳过 ():
(new D).logger(); // D
// 并且它可以正常运行

即使在内置类中,括号也是可选的:


(new Date).getDay();
(new Date).getMonth();
(new Date).getYear();

九、void 运算符

void 运算符与原型继承

void 是 JavaScript 中的关键字,用于评估语句并返回 undefined。

例如:


class D {
  logger() {
    return 89
  }
}

const d = new D

console.log(void d.logger()) // undefined

logger 方法应该返回 89,但是 void 关键字将使其无效并返回 undefined。

我曾读到过 undefined 之前可能会被赋予另一个值,从而伪造其语义。因此,使用 void 运算符可确保你得到一个真正的 undefined。它也常用于代码最小化目的。

十、通过 __proto__ 继承

__proto__ 是从 JavaScript 中的对象继承属性的方法。__proto__ 是 Object.prototype 的访问器属性,它公开访问对象的 [[Prototype]]。

此 __proto__ 将其 [[Prototype]] 中设置的对象的所有属性设置为目标对象。

让我们看一个例子:


const l = console.log
const obj = {
  method: function() {
    l("method in obj")
  }
}
const obj2 = {}
obj2.__proto__ = obj
obj2.method() // method in obj

我们有两个对象常量:obj 和 obj2。obj 具有 method 属性。obj2 是一个空的对象常量,即它没有属性。

我们访问 obj2 的 __proto__ 并将其设置为 obj。这会将通过 Object.prototype 可访问的 obj 的所有属性复制到 obj2。这就是为什么我们可以在 obj2 上调用方法而不会在没有定义的情况下得到错误的原因。

obj2 继承了 obj 的属性,因此 method 方法属性将在其属性中可用。

原型可用于对象,例如对象常量、对象、数组、函数、日期、RegEx、数字、布尔值、字符串。

一元运算符与指数运算

十一、一元运算符 +

一元 + 运算符将其操作数转换为数字类型。


+"23" // 23
+{} // NaN
+null // 0
+undefined // NaN
+{ valueOf: () => 67 } // 67
+"nnamdi45" // NaN

当我们希望将变量快速转换为 Number 时,这非常方便。

十二、一元运算符 -

一元运算符 - 将其操作数转换为 Number 类型,然后取反。

该运算符将一元 + 运算符的结果取反。首先,它将操作数转换为其 Number 值,然后取反该值。


-"23" // -23

此处发生的是,字符串 "23" 将转换为其数字类型,从而得到 23。然后,此正数将转换为其负数形式 -23。


-{} // NaN
-null // -0
-undefined // NaN
-{ valueOf: () => 67 } // -67
-"nnamdi45" // NaN

如果转换为数值的结果为 NaN,则不会应用取反。

取负 +0 产生 -0,取负 -0 产生 +0。


- +0 // -0
- -0 // 0

十三、指数运算符 **

该运算符用于指定数字的指数。

在数学中, 2^3^ 意味着将 2 乘以三次:


2 * 2 * 2

我们可以使用 ** 运算符在 JavaScript 中进行相同的操作:


2 ** 3 // 8
9 ** 3 // 729

总结

本文翻译自 11+ JavaScript Features You've Probably Never Used

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章