从parseInt的坑到JS原型的奇幻世界

169 阅读5分钟

一、parseInt的陷阱:你以为的转换可能并不简单

让我们从一个看似简单却暗藏玄机的问题开始:

['1','2','3'].map(parseInt) // 输出什么?

很多同学会不假思索地回答:"[1, 2, 3]呗!",然而实际结果却是:

[1, NaN, NaN]

为什么会这样?

这里涉及到两个关键知识点:

  1. map函数的回调参数map的回调函数实际上接收三个参数:(当前元素, 索引, 原数组)。所以当我们直接传入parseInt时,实际上是这样执行的:
parseInt('1', 0)  // 1
parseInt('2', 1)  // NaN
parseInt('3', 2)  // NaN
  1. parseInt的第二个参数parseInt(string, radix)中,radix表示要解析的数字的基数(进制),范围是2-36。当radix为0或未指定时,JS会根据字符串前缀来判断(0x开头为16进制,0开头为8进制或10进制,其他为10进制)。

所以:

  • parseInt('1', 0):radix为0,按10进制解析 → 1
  • parseInt('2', 1):1进制不存在(最小是2进制)→ NaN
  • parseInt('3', 2):2进制只允许0和1 → NaN

正确的写法

['1','2','3'].map(num => parseInt(num)) // [1, 2, 3]

或者更简洁:

['1','2','3'].map(Number) // [1, 2, 3]

这个小例子告诉我们:JS中看似简单的API,如果不了解其细节,很容易掉坑里。接下来我们要探讨的JS原型系统,更是充满了这样的"惊喜"。

二、JS原型:没有类的OOP魔法

1. 为什么JS需要原型?

在传统面向对象语言(如Java)中,我们有"类"的概念。但JS在设计之初为了简单,没有采用类的概念,而是通过原型(prototype)来实现对象继承。

想象一下:你是一个JS设计师,老板要求你实现面向对象,但不准用"class"关键字,你会怎么做?

2. 构造函数:身兼两职的"类"

JS的解决方案很聪明:用普通函数来充当"类"的角色:

function Person(name, age) {
    this.name = name
    this.age = age
}

这里Person既是普通函数,又是"类"的构造函数。怎么区分呢?约定俗成:构造函数首字母大写

3. new的背后:一场精心策划的"造人"仪式

当我们执行new Person('Alice', 30)时,背后发生了什么?让我们拆解这个"造人"过程:

  1. 新建空对象const obj = {}
  2. 设置原型obj.__proto__ = Person.prototype
  3. 绑定thisPerson.call(obj, 'Alice', 30)
  4. 返回对象(如果构造函数没有显式返回)

用代码表示:

function myNew(constructor, ...args) {
    const obj = {}
    obj.__proto__ = constructor.prototype
    const result = constructor.apply(obj, args)
    return result instanceof Object ? result : obj
}

4. 原型链:JS的继承秘籍

每个JS对象都有一个隐藏属性__proto__(现在更推荐用Object.getPrototypeOf()),指向它的原型对象。当访问对象属性时,如果对象自身没有,就会去原型上找,如果还没有,就去原型的原型上找...直到Object.prototype.__proto__为null。

这就形成了一条原型链,JS的继承就是基于此实现的。

function Person() {}
Person.prototype.sayHello = function() { console.log('Hello!') }

const p = new Person()
p.sayHello() // 1. p自身没有sayHello → 2. 去p.__proto__(Person.prototype)上找 → 找到!

5. 有趣的三角关系

在JS原型系统中,存在一个有趣的"三角恋"关系:

function Person() {}
const p = new Person()

console.log(p.__proto__ === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true
console.log(Person.__proto__ === Function.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__) // null

这就像:

  • 实例(p)暗恋(__proto__)构造函数的原型(Person.prototype)
  • 构造函数的原型(Person.prototype)心系(constructor)构造函数本身(Person)
  • 而构造函数(Person)又暗恋(__proto__)Function的原型(Function.prototype)

三、原型的实战应用

1. 方法共享:节约内存的利器

通过原型添加方法,所有实例共享同一方法,节省内存:

function Person(name) {
    this.name = name
}

// 方法放在原型上
Person.prototype.sayName = function() {
    console.log(this.name)
}

const p1 = new Person('Alice')
const p2 = new Person('Bob')

// 两个实例共享同一个sayName方法
console.log(p1.sayName === p2.sayName) // true

2. 原型继承:比class更灵活的继承

在ES6之前,我们这样实现继承:

function Parent(name) {
    this.name = name
}
Parent.prototype.sayName = function() {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name) // 调用父类构造函数
    this.age = age
}

// 设置原型链
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

// 添加子类方法
Child.prototype.sayAge = function() {
    console.log(this.age)
}

const c = new Child('Alice', 10)
c.sayName() // Alice
c.sayAge() // 10

3. 猴子补丁:谨慎使用的黑魔法

通过修改内置对象的原型,可以给所有实例添加方法(慎用!):

// 给所有数组添加sum方法
Array.prototype.sum = function() {
    return this.reduce((a, b) => a + b, 0)
}

console.log([1, 2, 3].sum()) // 6

注意:这会污染全局原型,可能导致命名冲突,一般只在polyfill时使用。

四、ES6 class:原型的语法糖

ES6引入了class语法,但底层仍然是基于原型的:

class Person {
    constructor(name) {
        this.name = name
    }
    
    sayName() {
        console.log(this.name)
    }
}

typeof Person // "function" - 类本质还是函数
Person.prototype.sayName // [Function: sayName] - 方法还是在原型上

class只是让原型写法更直观,没有引入新的继承模型。

五、原型相关面试题

1. 实现一个new

function myNew(constructor, ...args) {
    const obj = Object.create(constructor.prototype)
    const result = constructor.apply(obj, args)
    return result instanceof Object ? result : obj
}

2. 实现继承

function inherit(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype)
    Child.prototype.constructor = Child
}

3. instanceof原理

function myInstanceof(obj, constructor) {
    let proto = Object.getPrototypeOf(obj)
    while (proto) {
        if (proto === constructor.prototype) return true
        proto = Object.getPrototypeOf(proto)
    }
    return false
}

六、总结

JS的原型系统就像一座冰山:

  • 水面上的部分是简单的API和语法
  • 水面下则是复杂的原型链和继承机制

理解原型,才能真正理解JS的面向对象。记住几个关键点:

  1. 每个对象都有__proto__指向其原型
  2. 每个函数都有prototype属性
  3. 原型链的终点是Object.prototype.__proto__即null
  4. ES6 class只是语法糖,底层还是原型继承

最后,回到我们开头的parseInt问题,JS中很多"奇怪"行为都是因为我们没有完全理解其机制。深入理解原型,就是深入理解JS的核心设计思想。