JavaScript 中鲜为人知的 35 个核心特性与陷阱
本文深入解析 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 奇葩知识的资料请关注本站其它相关文章!
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
