JavaScript 中鲜为人知的 35 个核心特性与陷阱

作者:拾光时间:2026-10-25 00:23:15浏览:179

本文深入解析 JavaScript 中 35 个常被忽视的核心特性,涵盖原型链机制、类型转换陷阱、执行上下文及语言规范细节。从 Function.prototype 的类型到 NaN 的比较,从构造函数返回值到 setTimeout 嵌套延迟,逐一剖析其底层原理与最佳实践,帮助开发者避开常见陷阱,写出更健壮、高效的代码。

资深开发者在长期的编码实践中积累了大量经验,但也可能遇到一些极难触及的边界情况。这些情况通常不会出现在常规的业务逻辑中,因此容易被忽略。

1

Function.prototype 的类型实际上是函数(function),而普通自定义函数的 prototype 属性则是对象(object)。


typeof Function.prototype === 'function'; // true

function People() {}
typeof People.prototype === 'object';   // true

基于这一特性,我们可以通过 Function.prototype 来创建一个空函数,或者使用箭头函数来实现相同效果。


// OK 
const noop = Function.prototype;
// OK
const noop = () => {};

2

是否存在一个变量,其值不等于自身?


const x = NaN;
x !== x // true

NaN 是 JavaScript 中唯一不等于自身的值。这是因为 NaN 代表的是一个非数值的范围,而非具体的数值。早期的 isNaN() 函数在传入字符串时会返回 true,该问题已在 ES6 中通过 Number.isNaN() 修复。


isNaN('abc');    // true
Number.isNaN('abc') // false

为了兼容旧版浏览器,使用 x !== x 来判断变量是否为 NaN 是一种有效的方案。

3

构造函数中返回新数据的行为。


// 不返回
function People() {}
const people = new People();  // People {}

// 返回数字
function People() {
 return 1;
}
const people = new People();  // People {}

// 返回新对象
function Animal() {
 return {
  hello: 'world',
 };
}
const animal = new Animal(); // { hello: 'world' }

在实例化构造函数时,如果返回的是非对象类型的值(如数字、字符串等),该返回值将被忽略,实例仍为对象类型;但如果返回的是对象,则返回该对象。

4

.call.call 的执行机制是什么?


function fn1() {
 console.log(1);
}

function fn2() {
 console.log(2);
}

fn1.call.call(fn2); // 2

fn1.call.call(fn2) 等效于 fn2.call(undefined)。无论添加多少个 .call,最终效果均一致,即调用目标函数的 call 方法。

5

实例后的对象能否再次被实例化?


function People() {}

const lili = new People();      // People {}
const lucy = new lili.constructor();  // People {}

由于实例的原型链指向构造函数的 prototype,通过向上查找特性,最终可以在 People.prototype 上找到构造函数 People 本身,因此可以再次实例化。

6

setTimeout 嵌套会产生什么影响?


console.log(0, Date.now());

setTimeout(() => {
 console.log(1, Date.now());
 setTimeout(() => {
  console.log(2, Date.now());
  setTimeout(() => {
   console.log(3, Date.now());
   setTimeout(() => {
    console.log(4, Date.now());
    setTimeout(() => {
     console.log(5, Date.now());
     setTimeout(() => {
      console.log(6, Date.now());
     });
    });
   });
  });
 });
});

在嵌套层级为 0-4 层时,setTimeout 的最小间隔为 1ms;当嵌套层级达到第 5 层时,最小间隔至少为 4ms。

7

ES6 函数默认参数会生成独立的作用域。


var x = 10;

function fn(x = 2, y = function () { return x + 1 }) {
 var x = 5;
 return y();
}

fn();  // 3

8

函数表达式(非函数声明)中的函数名不可被覆盖。


const c = function CC() {
 CC = 123;
 return CC;
};

c(); // Function

当然,如果设置var CC = 123,加声明关键词是可以覆盖的。

9

严格模式下,函数的 this 指向 undefined 而非 Window 对象。


// 非严格
function fn1() {
 return this;
}
fn1(); // Window

// 严格
function fn2() {
 'use strict';
 return this;
}
fn2(); // undefined

经过 webpack 打包的模块化代码通常默认启用严格模式。

10

可以使用按位操作进行取整。


var x = 1.23 | 0; // 1

由于按位操作仅支持 32 位整数,小数部分会被直接截断。

11

indexOf() 无需额外比较数字即可判断存在性。


const arr = [1, 2, 3];

// 存在,等效于 > -1
if (~arr.indexOf(1)) {

}

// 不存在,等效于 === -1
!~arr.indexOf(1);

按位操作效率较高且代码简洁,也可使用 ES6 的 includes()。但在开发需兼容旧浏览器的开源库时,建议使用 indexOf()。

12

getter/setter 可以动态设置吗?


class Hello {
 _name = 'lucy';
 
 getName() {
  return this._name;
 }
 
 // 静态的getter
 get id() {
  return 1;
 }
}

const hel = new Hello();

hel.name;    // undefined
hel.getName(); // lucy

// 动态的getter
Hello.prototype.__defineGetter__('name', function() {
 return this._name;
});

Hello.prototype.__defineSetter__('name', function(value) {
 this._name = value;
});

hel.name;    // lucy
hel.getName(); // lucy

hel.name = 'jimi';
hel.name;    // jimi
hel.getName(); // jimi

13


0.3 - 0.2 !== 0.1 // true

浮点数运算存在精度误差,这是已知特性,通常可接受微小误差。


0.3 - 0.2 - 0.1 <= Number.EPSILON // true

14

class 语法糖的继承机制是怎样的?


function Super() {
 this.a = 1;
}

function Child() {
 // 属性继承
 Super.call(this);
 this.b = 2;
}
// 原型继承
Child.prototype = new Super();

const child = new Child();
child.a; // 1

正式代码中的原型继承不会直接实例化父类,而是实例化一个空函数,以避免重复声明动态属性。


const extends = (Child, Super) => {
 const fn = function () {};
 
 fn.prototype = Super.prototype;
 Child.prototype = new fn();
 Child.prototype.constructor = Child;
};

15

ES6 支持对对象进行重复解构。


const obj = {
 a: {
  b: 1
 },
 c: 2
};

const { a: { b }, a } = obj;

一行代码可同时获取 a 和 a.b。若需多次使用,常规做法是先解构 a,再解构 b。

16

如何判断代码是否经过压缩?


function CustomFn() {}

const isCrashed = typeof CustomFn.name === 'string' && CustomFn.name === 'CustomFn';

17

对象之间的 === 比较的是内存地址,而 >= 比较的是转换后的值。


{} === {} // false

// 隐式转换 toString()
{} >= {} // true

18

instanceof 的判断依据是原型是否存在于对象的原型链中。


function People() {}
function Man() {}
Man.prototype = new People();
Man.prototype.constructor = Man;

const man = new Man();
man instanceof People;  // true

// 替换People的原型
People.prototype = {};
man instanceof People;  // false

若使用 ES6 的 class,prototype 属性不可重新定义,因此不会出现上述情况。

19


Object.prototype.__proto__ === null; // true

原型链查找的最顶层是 null。

20

parseInt 处理极小数字时可能产生 bug。


parseInt(0.00000000454); // 4
parseInt(10.23);     // 10

21


1 + null     // 1
1 + undefined   // NaN

Number(null)   // 0
Number(undefined) // NaN

22

实参 arguments 和形参保持同步关系。


function test(a, b, c) {
 console.log(a, b, c); // 2, 3, undefined
 
 arguments[0] = 100;
 arguments[1] = 200;
 arguments[2] = 300;
 
 console.log(a, b, c); // 100, 200, undefined
}
test(2, 3);

若实参个数不足,同步关系失效。在严格模式下,arguments 仅为副本,可避免此行为。

23

void 运算符的行为特性。


void 0 === undefined     // true
void 1 === undefined     // true
void {} === undefined     // true
void 'hello' === undefined  // true
void void 0 === undefined   // true

void 运算符与任何值均无关联,仅用于忽略表达式结果。

24

try/catch/finally 的执行顺序。


function fn1() {
 console.log('fn1');
 return 1;
}

function fn2() {
 console.log('fn2');
 return 2;
}

function getData() {
 try {
  throw new Error('');
 } catch (e) {
  return fn1();
 } finally {
  return fn2();
 }
}

console.log(getData());

// 打印顺序: 'fn1', 'fn2', 2

在 try/catch 中若遇到 return,返回值会先存入临时变量,执行 finally 后再返回该临时变量。若 finally 中也有 return,则立即返回新值。

25

是否存在变量 x,使其等于多个数字?


const x = {
 value: 0,
 toString() {
  return ++this.value;
 }
}

x == 1 && x == 2 && x == 3;  // true

通过隐式转换可以实现这一效果。

26

clearTimeout 和 clearInterval 可以互换使用吗?


var timeout = setTimeout(() => console.log(1), 1000);
var interval = setInterval(() => console.log(2), 800);

clearInterval(timeout);
clearTimeout(interval);

答案是肯定的。大多数浏览器支持互相清理定时器,但建议使用对应的清理函数以保持代码清晰。

27

以下代码的打印顺序是什么?


setTimeout(() => {
 console.log(1);
}, 0);

new Promise((resolve) => {
 console.log(2);
 resolve();
}).then(() => console.log(3));

function callMe() {
 console.log(4);
}

(async () => {
 await callMe();
 console.log(5);
})();

答案是:2, 4, 3, 5, 1。

主线任务:2,4;微任务:3,5;宏任务:1。

28

null 属于 object 类型,但并非继承自 Object,这更像是一个历史遗留问题。由于大量代码依赖此特性,修复它可能导致广泛错误。


typeof null === 'object';       // true
Object.prototype.toString.call(null); // [object Null]
null instanceof Object;        // false

29

基本类型(null 和 undefined 除外)在操作时,引擎会自动将其包装为对象,操作完成后销毁该对象。


'abc'.substr(1);
(123).toFixed(2);

因此,向基本类型添加属性会被丢弃,除非修改原型链。


const data = 'abc';
data.x = 'y';
console.log(data.x); // undefined

data.__proto__.x = 'z';
console.log(data.x); // 'z'

30

数据超过安全值后变得不安全。


Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // true

// 等价于
2 ** 53 === 2 ** 53 + 1;   // true

31

函数形参带默认值时会改变某些认知。


function test(a, b = 1) {
 // 别名同步,非严格模式预期可以同步
 arguments[0] = 20;
 console.log(a);      // 2
}
// 检测函数的形参个数,预期值为:2
console.log(test.length);  // 1

test(123);

32

所有数字均为浮点类型。位运算时,JavaScript 会先将数字转换为整数类型,这相比其他语言会产生额外的性能开销。


1 | 0      // 1
1.234 | 0    // 1
1.234 | 0.6   // 1

1 & 1      // 1
1.23 & 1.456  // 1

~1       // -2
~1.234     // -2

33

赋值给 location 可直接实现页面跳转。


location = 'http://baidu.com';

34

new 关键字的另一个用法。


function Test() {
 console.log(new.target === Test); // true
}

new Test();

若实例化子类,new.target 将指向子类而非父类。利用此特性可实现抽象类的效果。

35

+0 和 -0 存在区别。


1/+0 === Infinity
1/-0 === -Infinity

以上便是 JavaScript 中鲜为人知的 35 个核心特性与陷阱的详细内容,更多关于 JavaScript 奇葩知识的资料请关注本站其它相关文章!

作者最新文章