原生 JS 灵魂之问,请问你能接得住几个?(上)
Category(分类): JavaScript Status: 已整理
本文保留原文按“数据类型 → 类型检测 → 类型转换 → 闭包 → 原型链 → 继承”提问的学习路线。示例中的输出按当前 ECMAScript 语义修正;手写实现主要用于理解机制,不应替代标准内置方法。
第一篇:JS 数据类型之问——概念篇
1. JS 原始数据类型有哪些?引用数据类型有哪些?
当前 ECMAScript 有 7 种原始类型:
undefinednullbooleannumberbigintstringsymbol
除此之外的是对象类型,包括普通对象、数组、函数、正则表达式、日期、Map、Set、Promise、DOM 对象等。函数也是对象的一种,但 typeof function () {} 会返回特殊的字符串 "function"。
“基本类型放栈、引用类型放堆”是便于入门的实现想象,不是 ECMAScript 对引擎内存布局的规定。引擎可以根据逃逸分析、优化和垃圾回收策略决定实际存储方式。真正稳定的语言层结论是:原始值不可变,变量保存的是值;对象变量保存对对象的引用值。
2. 说出下面运行的结果,解释原因
function test(person) {
person.age = 26
person = {
name: 'hzj',
age: 18,
}
return person
}
const p1 = {
name: 'fyq',
age: 19,
}
const p2 = test(p1)
console.log(p1) // { name: 'fyq', age: 26 }
console.log(p2) // { name: 'hzj', age: 18 }
JavaScript 的参数传递始终是按值传递。对象参数的值是一个引用值:修改 person.age 会通过这个引用修改原对象;但给局部参数 person 重新赋值,不会改变调用者变量 p1 保存的引用。
3. null 是对象吗?为什么?
结论:null 是一种原始值,不是对象。
console.log(typeof null) // 'object'
console.log(null === null) // true
typeof null === 'object' 是 JavaScript 早期实现留下的历史行为。不要根据 typeof 的结果把 null 当作对象;判断对象通常需要先排除 null:
function isObject(value) {
return value !== null && typeof value === 'object'
}
4. '1'.toString() 为什么可以调用?
对原始字符串调用属性或方法时,规范会在需要时执行临时的包装/装箱行为,使它能够访问 String.prototype 上的方法;这不意味着原始字符串永久变成了对象。
const value = '1'
console.log(value.toString()) // '1'
console.log(typeof value) // 'string'
类似地,数字和布尔值也可以访问相应包装原型的方法。Symbol 和 BigInt 不能使用 new Symbol()、new BigInt() 创建实例,但它们仍可以在需要时进行临时包装:
console.log((1n).toString())
console.log(Object(1n)) // BigInt 包装对象
“包装对象”与原始值不是同一种值:
console.log(new String('a') === 'a') // false
console.log(Object('a') === 'a') // false
5. 0.1 + 0.2 为什么不等于 0.3?
JavaScript 的 number 使用 IEEE 754 双精度二进制浮点数。很多十进制小数无法用有限长度的二进制小数精确表示,因此运算会在表示范围内进行舍入:
console.log(0.1 + 0.2) // 0.30000000000000004
金额等对精度敏感的场景,应使用最小货币单位的整数、定点/十进制定点库,或在支持的环境中谨慎使用 decimal 相关方案,而不是直接用二进制浮点数比较相等。
6. 如何理解 BigInt?
BigInt 用于表示任意精度的整数,适合超过 Number.MAX_SAFE_INTEGER 的整数计算,但它不是浮点数,也不能直接与 number 混合进行大多数算术运算。
console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991
console.log(9007199254740993) // 9007199254740992(精度已丢失)
console.log(9007199254740993n) // 9007199254740993n
console.log(9007199254740992 === 9007199254740993) // true
创建 BigInt 可以使用字面量后缀 n 或 BigInt():
const a = 9007199254740995n
const b = BigInt('9007199254740995')
console.log(a + b)
console.log(20n / 3n) // 6n,向零截断
需要注意:
10n + 10会抛出TypeError,应显式转换后再运算。10n / 3n不能得到小数结果。Math.max(2n, 4n)会抛出TypeError,很多 Web API 也要求传入number。- BigInt 不支持一元加号
+10n;可以使用BigInt()或其他明确转换方式。 - BigInt 可以参与布尔判断,
0n是假值,其他 BigInt 是真值。 - BigInt 支持整数的比较和位运算,但不能与 Number 直接做位运算。
- JSON 没有原生 BigInt 表示;
JSON.stringify({ value: 1n })默认会抛错,需要自行转换为字符串或其他协议格式。
console.log(10n < 11) // true,比较运算允许适当的数值比较
console.log(Boolean(0n)) // false
console.log(Boolean(3n)) // true
try {
console.log(10n + 10)
} catch (error) {
console.log(error.name) // TypeError
}

原文图片反映的是 BigInt 刚推出时的兼容性状态;现在主流现代浏览器和 Node.js 已支持 BigInt,但发布到旧环境仍应检查目标运行时。图片已下载到本地,保留作历史资料。
第二篇:JS 数据类型之问——检测篇
1. typeof 是否能正确判断类型?
对原始值来说,typeof 大多能给出有用结果,但 null 是历史例外;对对象来说,除了函数、可调用代理等情况,通常都返回 "object":
console.log(typeof 1) // 'number'
console.log(typeof '1') // 'string'
console.log(typeof undefined) // 'undefined'
console.log(typeof true) // 'boolean'
console.log(typeof Symbol()) // 'symbol'
console.log(typeof 1n) // 'bigint'
console.log(typeof null) // 'object'
console.log(typeof []) // 'object'
console.log(typeof {}) // 'object'
console.log(typeof console.log) // 'function'
因此 typeof 适合做第一层判断,但不能单独区分数组、日期、Map 或普通对象。
2. instanceof 能否判断基本数据类型?
原生 instanceof 主要用于检查对象的原型链,不能直接把数字原始值当成 Number 实例:
console.log(111 instanceof Number) // false
console.log(new Number(111) instanceof Number) // true
不过,构造函数可以自定义 Symbol.hasInstance,因此可以定义自己的判断规则:
class PrimitiveNumber {
static [Symbol.hasInstance](value) {
return typeof value === 'number'
}
}
console.log(111 instanceof PrimitiveNumber) // true
这里改变的是 PrimitiveNumber 的 instanceof 行为,不是改变原生 Number 的内部语义。跨 iframe 判断数组、日期等对象时,也要注意每个 realm 有自己的构造函数,优先使用对应的品牌检测 API。
3. 能不能手动实现 instanceof?
下面是一个用于教学的简化实现。它没有覆盖所有代理、异常构造器和 Symbol.hasInstance 语义,生产代码应直接使用原生 instanceof:
function myInstanceof(value, Constructor) {
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
return false
}
if (typeof Constructor !== 'function') {
throw new TypeError('Right-hand side of instanceof is not callable')
}
const prototype = Constructor.prototype
if (typeof prototype !== 'object' || prototype === null) {
throw new TypeError('Constructor.prototype is not an object')
}
let current = Object.getPrototypeOf(value)
while (current !== null) {
if (current === prototype) return true
current = Object.getPrototypeOf(current)
}
return false
}
console.log(myInstanceof(new String('111'), String)) // true
console.log(myInstanceof('111', String)) // false
4. Object.is 和 === 的区别?
Object.is 使用 SameValue 语义;严格相等使用 Strict Equality Comparison。两者最常见的差异是 NaN 和 +0/-0:
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true
console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
Object.is 不是“更严格的深比较”,它仍然只比较两个值本身,不会递归比较对象内容:
console.log(Object.is({}, {})) // false
const object = {}
console.log(Object.is(object, object)) // true
第三篇:JS 数据类型之问——转换篇
1. [] == ![] 结果是什么?为什么?
console.log([] == ![]) // true
简化过程如下:
[]是对象,转换为布尔值时是真值,所以![]是false。==比较对象与布尔值时,会继续执行规范规定的类型转换。false转换为数字0。- 空数组经过
ToPrimitive得到空字符串,再转换为数字0。 - 最终比较
0 == 0,结果为true。
这也是为什么不建议在业务代码中依赖复杂的隐式类型转换。
2. JS 中类型转换有哪些?
“类型转换只有数字、布尔值、字符串三种”是便于入门的概括,不是规范中的完整分类。ECMAScript 还定义了 ToPrimitive、ToBoolean、ToNumber、ToNumeric、ToString、ToObject 等抽象操作。Symbol 和 BigInt 的转换边界尤其需要单独注意。

3. == 和 === 有什么区别?
=== 不进行一般的隐式类型转换;== 会按照 Abstract Equality Comparison 规则转换。常见规则包括:
- 类型相同,按严格相等继续比较。
null == undefined为true,但它们不等于其他普通值。- 字符串与数字比较时,字符串可能转换为数字。
- 布尔值比较时,会先转换为数字。
- 对象与字符串、数字、BigInt 等比较时,会先执行
ToPrimitive。 Symbol参与某些隐式转换时会抛出异常,不能把它当作普通字符串处理。
console.log(1 == '1') // true
console.log(1 === '1') // false
console.log(null == undefined) // true
console.log(null === undefined) // false
console.log({ valueOf: () => 1 } == 1) // true
现代业务代码通常优先使用 ===,但理解 == 有助于读懂旧代码和分析面试题。
4. 对象转原始类型依据什么流程?
对象转换为原始值时,会先调用 Symbol.toPrimitive(如果存在);否则根据 hint 选择 valueOf 和 toString 的顺序:
number/defaulthint:通常先valueOf,再toString。stringhint:通常先toString,再valueOf。- 如果最后仍未得到原始值,就抛出
TypeError。
const object = {
name: 'John',
[Symbol.toPrimitive](hint) {
if (hint === 'string') return this.name
return 42
},
}
console.log(String(object)) // John
console.log(+object) // 42
const object = {
valueOf() {
return 10
},
toString() {
return 'Hello'
},
}
console.log(+object) // 10
console.log(String(object)) // Hello
普通对象默认的 toString 会返回 "[object Object]",所以 +{} 的结果通常是 NaN,并不会因为转换失败而自动抛错:
console.log(`${{}}`) // [object Object]
console.log(+{}) // NaN
5. 如何让 if (a == 1 && a == 2) 成立?
可以利用对象参与 == 时会反复执行 ToPrimitive 的事实:
let value = 0
const a = {
valueOf() {
value++
return value
},
}
console.log(a == 1 && a == 2) // true
这是语言机制演示,不是推荐的业务写法;对象的转换副作用会让代码非常难以维护。
第四篇:谈谈你对闭包的理解
什么是闭包?
闭包可以理解为:函数与它能够访问的词法环境一起被保存,使函数在定义它的外层执行结束后仍能访问相关变量。函数不一定要“返回出来”才产生闭包,只要存在对外层变量的引用和可观察的延续使用即可。
function createCounter() {
let count = 0
return () => {
count++
return count
}
}
const next = createCounter()
console.log(next()) // 1
console.log(next()) // 2
闭包保存的是必要的词法环境,不应简单理解成“复制了整个父函数作用域”或“所有变量都永远放在堆中”。引擎可以进行优化,但只要闭包仍然可达,被闭包使用的状态就不能被回收。
闭包产生的原因
作用域链是词法结构:访问变量时,先查当前环境,再沿外层环境查找。下面的 f2 可以访问定义位置的 a:
function f1() {
const a = 2
function f2() {
console.log(a)
}
return f2
}
const x = f1()
x() // 2
闭包也可以通过外部变量保存回调形成:
let callback
function setup() {
const value = 2
callback = () => console.log(value)
}
setup()
callback() // 2
闭包的常见表现形式
- 返回函数。
- 把函数作为参数传给另一个函数。
- 定时器、事件监听器、网络回调和消息回调引用外层变量。
- IIFE 或模块通过私有词法环境隐藏状态。
function callLater() {
const message = 'done'
setTimeout(() => console.log(message), 0)
}
callLater()
并不是“只要是回调函数就必然保存了有用的外层状态”;关键在于它是否引用了定义位置的自由变量。
如何解决循环输出问题?
for (var i = 1; i <= 5; i++) {
setTimeout(() => console.log(i), 0)
}
常见输出是五个 6:计时器回调运行时,循环已经结束,所有回调读取的是同一个 var 绑定。
方法一:IIFE 创建新的参数绑定
for (var i = 1; i <= 5; i++) {
((value) => {
setTimeout(() => console.log(value), 0)
})(i)
}
方法二:使用计时器的额外参数
for (var i = 1; i <= 5; i++) {
setTimeout((value) => console.log(value), 0, i)
}
方法三:使用 let 的每次迭代绑定
for (let i = 1; i <= 5; i++) {
setTimeout(() => console.log(i), 0)
}
let 不会让作用域链“消失”;规范为 for 循环的每次迭代创建了新的词法绑定,因此每个回调捕获不同的 i。

第五篇:谈谈你对原型链的理解
1. 原型对象和构造函数有什么关系?
可作为普通构造器使用的函数和类通常具有可用的 prototype 属性;箭头函数、async 函数、Generator 函数和 bound function 等并不都能直接作为构造器。通过 new 调用时,实例的 [[Prototype]] 通常被设置为构造函数的 prototype。类也有原型对象,但类构造器不能像普通函数一样不使用 new 直接调用。
function Person(name) {
this.name = name
}
Person.prototype.say = function () {
return this.name
}
const person = new Person('Ada')
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(person.say()) // Ada
现代代码应使用 Object.getPrototypeOf 和 Object.setPrototypeOf 观察/修改原型,避免依赖非标准的 Object.prototype.__proto__ 访问器。
2. 什么是原型链?
对象读取属性时,如果自身没有该属性,JavaScript 会沿 [[Prototype]] 链继续查找,直到找到属性或到达 null:
const parent = { kind: 'parent' }
const child = Object.create(parent)
child.name = 'child'
console.log(child.kind) // parent
console.log(Object.hasOwn(child, 'kind')) // false
console.log('kind' in child) // true

Object.hasOwn(object, key)只检查对象自身属性。key in object会检查自身和原型链。- 原型方法中的
this取决于调用它的实例,不会固定为原型对象。
第六篇:JS 如何实现继承?
第一种:借助 call
function Parent1() {
this.name = 'parent1'
}
function Child1() {
Parent1.call(this)
this.type = 'child1'
}
console.log(new Child1())
子实例可以得到父构造函数写入的自有属性,但不能自动访问 Parent1.prototype 上的方法。这种技术也称借用构造函数,只是复用初始化逻辑,不是完整的原型继承。
第二种:直接借助原型链
function Parent2() {
this.name = 'parent2'
this.play = [1, 2, 3]
}
Parent2.prototype.say = function () {
return this.name
}
function Child2() {}
Child2.prototype = new Parent2()
const s1 = new Child2()
const s2 = new Child2()
s1.play.push(4)
console.log(s1.play) // [1, 2, 3, 4]
console.log(s2.play) // [1, 2, 3, 4]:共享了原型上的数组
此外,定义原型时执行了一次父构造函数,Child2.prototype.constructor 也没有自动恢复为 Child2。这是历史模式,现代代码不应直接使用。

第三种:组合继承
function Parent3() {
this.name = 'parent3'
this.play = [1, 2, 3]
}
Parent3.prototype.say = function () {
return this.name
}
function Child3() {
Parent3.call(this)
this.type = 'child3'
}
Child3.prototype = new Parent3()
Child3.prototype.constructor = Child3
const s3 = new Child3()
const s4 = new Child3()
s3.play.push(4)
console.log(s3.play) // [1, 2, 3, 4]
console.log(s4.play) // [1, 2, 3]
组合继承解决了实例之间共享可变自有属性的问题,但 Parent3 在设置 Child3.prototype 和创建每个实例时各执行一次。
第四种:共享原型(不推荐)
function Parent4() {}
function Child4() {}
Child4.prototype = Parent4.prototype
这种方式虽然避免了额外构造,但父子构造器共享同一个 prototype。给 Child4.prototype 增加方法会影响 Parent4,constructor 和类型关系也无法表达清楚。它不是正常的继承实现。
第五种:寄生组合式继承
function Parent5(name) {
this.name = name
this.play = [1, 2, 3]
}
Parent5.prototype.say = function () {
return this.name
}
function Child5(name, type) {
Parent5.call(this, name)
this.type = type
}
Child5.prototype = Object.create(Parent5.prototype, {
constructor: {
configurable: true,
value: Child5,
writable: true,
},
})
const child = new Child5('parent5', 'child5')
console.log(child.say()) // parent5
console.log(child instanceof Parent5) // true
console.log(child.constructor === Child5) // true
这就是传统 ES5 中更合理的寄生组合式写法:父构造函数负责实例状态,Object.create 负责建立原型关系,避免 new Parent5() 的额外调用。
ES6 的 extends
现代代码通常直接使用 class extends:


class Parent {
constructor(name) {
this.name = name
this.play = [1, 2, 3]
}
say() {
return this.name
}
}
class Child extends Parent {
constructor(name, type) {
super(name)
this.type = type
}
}
const child = new Child('Ada', 'student')
console.log(child.say())
console.log(child instanceof Parent) // true
class 仍然建立在原型机制之上,但还提供派生构造器、静态继承、私有元素和严格模式等语言语义。Babel 等转译器可能使用 Object.create、Object.setPrototypeOf 和辅助函数模拟一部分行为,但转译代码不能简单等同于原生 class 的所有内部语义。
面向对象一定是好的设计吗?
不一定。继承适合稳定的 is-a 关系,但它会带来强耦合:子类通常得到父类暴露的整套接口,父类变化可能影响多个子类。原文用“汽车与新能源汽车”说明这一点,这个例子仍然有价值,但不应得出“继承永远不好”的结论。
组合可以按需拼装能力:
function drive() {
return 'wuwuwu'
}
function music() {
return 'lalala'
}
function addOil() {
return 'add oil'
}
function compose(...behaviors) {
return Object.assign({}, ...behaviors.map((behavior) => ({
[behavior.name]: behavior,
})))
}
const car = compose(drive, music, addOil)
const newEnergyCar = compose(drive, music)
console.log(Object.keys(car))
console.log(Object.keys(newEnergyCar))
实际项目中还可以使用函数、模块、对象组合、依赖注入和接口约束。稳定的类型层次可以使用 class extends;横向能力复用通常更适合组合或明确的 mixin。
小结
- 原始值、对象引用值和引擎的实际内存布局要区分。
Array.isArray、Object.is等标准 API 通常比手写原型判断更可靠。- 闭包保存的是可访问的词法环境,不等于“返回函数才有闭包”。
- 原型链是属性查找机制;
constructor不是可靠的类型标签。 - ES5 继承模式适合理解历史机制,现代项目优先考虑
class、组合和明确的对象能力边界。
参考出处:
- ES5 实现继承那些事
- 相关转载:原生 JS 的知识系统梳理
- JS 最新基本数据类型:BigInt
- MDN:JavaScript 语法与类型
- MDN:闭包
- MDN:继承与原型链
- ECMAScript:类型转换与比较
配图说明:图示按原文顺序从 article.rivers.pub 的历史文章图片地址下载到本地 images/88-image-*,图片并非规范官方图,公开发布前仍需核实原始授权。