一、parseInt的陷阱:你以为的转换可能并不简单
让我们从一个看似简单却暗藏玄机的问题开始:
['1','2','3'].map(parseInt) // 输出什么?
很多同学会不假思索地回答:"[1, 2, 3]呗!",然而实际结果却是:
[1, NaN, NaN]
为什么会这样?
这里涉及到两个关键知识点:
- map函数的回调参数:
map的回调函数实际上接收三个参数:(当前元素, 索引, 原数组)。所以当我们直接传入parseInt时,实际上是这样执行的:
parseInt('1', 0) // 1
parseInt('2', 1) // NaN
parseInt('3', 2) // NaN
- parseInt的第二个参数:
parseInt(string, radix)中,radix表示要解析的数字的基数(进制),范围是2-36。当radix为0或未指定时,JS会根据字符串前缀来判断(0x开头为16进制,0开头为8进制或10进制,其他为10进制)。
所以:
parseInt('1', 0):radix为0,按10进制解析 → 1parseInt('2', 1):1进制不存在(最小是2进制)→ NaNparseInt('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)时,背后发生了什么?让我们拆解这个"造人"过程:
- 新建空对象:
const obj = {} - 设置原型:
obj.__proto__ = Person.prototype - 绑定this:
Person.call(obj, 'Alice', 30) - 返回对象(如果构造函数没有显式返回)
用代码表示:
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的面向对象。记住几个关键点:
- 每个对象都有
__proto__指向其原型 - 每个函数都有
prototype属性 - 原型链的终点是
Object.prototype.__proto__即null - ES6 class只是语法糖,底层还是原型继承
最后,回到我们开头的parseInt问题,JS中很多"奇怪"行为都是因为我们没有完全理解其机制。深入理解原型,就是深入理解JS的核心设计思想。