一、一门在 10 天内创造出来的语言
1995 年,网景公司的 Brendan Eich 接到一个任务:给浏览器开发一门脚本语言,让网页能“动起来”。上层给他的时间只有 10 天。
10 天能做出来的东西,注定不可能完美。但 Eich 在这 10 天里做的设计决定,影响了此后近三十年的 Web 开发格局。他选择了三个设计方向:
- 语法像 Java:当时 Java 正如日中天,网景希望借 Java 的热度推广这门新语言。
- 函数式编程的特性:函数是第一等公民(first-class function),可以赋值给变量、作为参数传递、作为返回值返回。
- 基于原型的面向对象:不需要“类”的概念,对象可以直接继承自另一个对象。
语言最初叫 LiveScript,后来因为营销策略改名为 JavaScript。但JavaScript 和 Java 除了名字相似和表面语法相近,本质上完全是两门语言。这个命名的后遗症延续至今——无数人因为这个名字以为“JavaScript 是 Java 的简化版”,这是对 JavaScript 最大的误解之一。
二、原型继承:JavaScript 的面向对象之道
大多数主流语言(Java、C++、C#)使用类继承:先定义类(Class),然后从类实例化出对象(Instance)。类是模板,对象是产品。继承是类与类之间的关系。
JavaScript 走了完全不同的一条路:原型继承。在 JavaScript 中,没有类的概念(ES6 的 class 只是语法糖,后面会详讲),只有对象。对象可以直接继承自另一个对象。被继承的对象称为原型。
原型链:查找属性的路径
当你访问一个对象的属性时,JavaScript 引擎会:
- 先在对象本身上查找这个属性。
- 如果找不到,就去对象的原型(
__proto__指向的对象)上找。 - 如果还找不到,就去原型的原型上找。
- 一直沿着这条“原型链”往上找,直到找到属性或到达链的顶端(
null)。
这条查找路径,就是原型链。
用代码理解原型链
// 创建一个普通对象作为原型
let animal = {
type: '动物',
breathe: function() {
console.log('呼吸中...');
}
};
// 以 animal 为原型创建一个新对象
let dog = Object.create(animal);
dog.name = '旺财';
dog.bark = function() {
console.log('汪汪!');
};
// 属性查找
console.log(dog.name); // '旺财' —— 在 dog 自身找到
console.log(dog.type); // '动物' —— dog 没有,沿原型链在 animal 上找到
dog.breathe(); // '呼吸中...' —— 同样沿原型链找到
console.log(dog.toString()); // '[object Object]' —— animal 也没有,继续沿链在 Object.prototype 上找到
关键认知:dog 自己没有 type 和 breathe,但通过原型链,它可以“借用” animal 上的属性和方法。这不是复制——dog 和 animal 之间是引用关系。如果你修改了 animal.type,dog.type 的读取结果也会变(除非 dog 自己有同名的属性覆盖了它)。
__proto__ vs prototype:最容易混淆的两个属性
这是 JavaScript 中最让初学者困惑的概念对。用一句话区分:
__proto__:每个对象都有这个属性(更准确地说是内部槽[[Prototype]]),它指向该对象的原型。这是原型链的“链接”。prototype:只有函数才有这个属性(更准确地说,是函数对象才有)。它指向一个对象,当这个函数被用作构造函数(用new调用)时,新创建的对象的__proto__会指向这个prototype对象。
function Person(name) {
this.name = name;
}
// Person 是函数,所以有 prototype 属性
console.log(Person.prototype); // { constructor: Person }
// 用 new 调用 Person,创建实例
let p = new Person('张三');
// p 是普通对象,它的 __proto__ 指向 Person.prototype
console.log(p.__proto__ === Person.prototype); // true
// Person.prototype 本身也是对象,它的 __proto__ 指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype); // true
// Object.prototype 的原型是 null,原型链的终点
console.log(Object.prototype.__proto__); // null
图解原型链:
p (实例)
→ __proto__ → Person.prototype
→ __proto__ → Object.prototype
→ __proto__ → null
记忆口诀:prototype 是函数给未来实例准备的“模板”,__proto__ 是对象指向它实际原型的“链接”。
三、“一切皆对象”这句话对吗?
你经常听到“JavaScript 中一切皆对象”。这句话既对又不对。
对的部分:JavaScript 中几乎所有东西,最终都能追溯到 Object.prototype。数组、函数、正则表达式,它们本质上都是对象:
console.log(typeof []); // 'object'
console.log(typeof {}); // 'object'
console.log(typeof /regex/); // 'object'
console.log(typeof function(){}); // 'function' —— 但函数本身也是对象,可以加属性
不对的部分:JavaScript 有七种基本类型(primitive types),它们不是对象:
string:字符串number:数字boolean:布尔值nullundefinedsymbol(ES6 新增)bigint(ES2020 新增)
基本类型不是对象,没有属性。但你可能会问:为什么 'hello'.toUpperCase() 能工作?因为 JavaScript 引擎在调用方法时会临时包装:把基本类型 'hello' 包装成 String 对象,调用方法,然后立即销毁包装对象。这就是“基本类型包装对象”机制。
let str = 'hello';
// 当执行 str.toUpperCase() 时,引擎内部做了类似以下操作:
// let temp = new String(str); // 临时包装
// temp.toUpperCase(); // 调用方法
// temp = null; // 销毁包装
更准确的说法:JavaScript 中,除了基本类型以外的所有值,都是对象。
四、构造函数与 new 关键字
在 ES6 引入 class 之前,创建“类”的方式是构造函数:
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在 prototype 上定义方法,让所有实例共享
Person.prototype.sayHi = function() {
console.log('你好,我是' + this.name);
};
let p1 = new Person('张三', 28);
let p2 = new Person('李四', 22);
p1.sayHi(); // '你好,我是张三'
p2.sayHi(); // '你好,我是李四'
console.log(p1.sayHi === p2.sayHi); // true —— 共享同一个方法,节省内存
new 到底做了什么?
当你写 new Person('张三', 28) 时,引擎执行了以下步骤:
- 创建一个空对象
{}。 - 把这个空对象的
__proto__指向Person.prototype。 - 以这个空对象为
this,调用Person函数(执行this.name = '张三'等赋值)。 - 如果
Person函数有return且返回了一个对象,就返回那个对象;否则返回这个新创建的对象。
理解 new 的这四步,你就能看穿 class 语法糖的本质——下一节就讲。
五、ES6 class:语法糖还是真正的类?
ES6(2015 年)引入了 class 关键字,让从 Java/C++ 转过来的开发者倍感亲切:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log('你好,我是' + this.name);
}
}
class Student extends Person {
constructor(name, age, school) {
super(name, age); // 调用父类的 constructor
this.school = school;
}
sayHi() {
super.sayHi(); // 调用父类的方法
console.log('我在' + this.school + '上学');
}
}
let s = new Student('王五', 18, '清华大学');
s.sayHi();
// 你好,我是王五
// 我在清华大学上学
看起来很优雅,类、继承、super、extends 一应俱全。但本质上,这仍然是原型继承:
console.log(typeof Student); // 'function' —— class 本质是函数
console.log(s.__proto__ === Student.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true
class 只是对构造函数 + 原型链模式的语法封装。底层机制没变,变的是写法。理解这一点很重要:当你用 class 时,你实际在操作的东西仍然是原型链。
原生原型继承 vs class 语法:
| 机制 | 原生方式(ES5) | class 方式(ES6+) |
|---|---|---|
| 定义构造函数 | function Person() {} |
class Person { constructor() {} } |
| 定义方法 | Person.prototype.sayHi = function() {} |
直接在 class 体内写 sayHi() {} |
| 继承 | Student.prototype = Object.create(Person.prototype) |
class Student extends Person {} |
| 调用父类构造 | Person.call(this, name, age) |
super(name, age) |
对于日常开发,使用 class 是更好的选择——代码更清晰、更不易出错。但理解底层原型链,能帮你解释为什么某些“看起来应该对”的代码却出了问题。
六、this 的动态绑定
JavaScript 的 this 和其他语言的行为不同。在 Java 或 C++ 中,this 始终指向当前类的实例,是静态确定的。在 JavaScript 中,this 的值取决于函数如何被调用,而不是函数在哪里定义。
这被称为动态绑定。调用方式决定了 this 的指向:
function showThis() {
console.log(this);
}
// 1. 作为普通函数调用 → this 指向全局对象(严格模式下为 undefined)
showThis(); // window(浏览器环境)
// 2. 作为对象的方法调用 → this 指向该对象
let obj = { name: 'obj', show: showThis };
obj.show(); // { name: 'obj', show: f }
// 3. 作为构造函数调用(用 new)→ this 指向新创建的对象
new showThis(); // showThis {}
// 4. 用 call / apply / bind 显式指定 this
showThis.call({ name: 'custom' }); // { name: 'custom' }
最常见的陷阱:把方法作为回调传递时,this 会丢失:
let user = {
name: '张三',
sayHi: function() {
console.log('你好,' + this.name);
}
};
// 正常调用
user.sayHi(); // '你好,张三'
// 作为回调传递——this 丢失
setTimeout(user.sayHi, 1000); // '你好,undefined' —— this 指向了全局对象
// 解决方案一:用 bind 绑定 this
setTimeout(user.sayHi.bind(user), 1000); // '你好,张三'
// 解决方案二:用箭头函数(箭头函数不绑定自己的 this)
setTimeout(() => user.sayHi(), 1000); // '你好,张三'
关于 this 的完整分析,我们会在执行上下文篇中深入展开。现在只需要记住核心原则:this 取决于调用方式,而不是定义位置。
七、JavaScript 的类型体系全景
最后,我们用一张全景图来总结 JavaScript 的类型体系:
JavaScript 中的值
├── 基本类型(Primitive Types)
│ ├── string
│ ├── number
│ ├── boolean
│ ├── null
│ ├── undefined
│ ├── symbol(ES6)
│ └── bigint(ES2020)
│
└── 对象类型(Object Types)
├── 普通对象(Object)
├── 数组(Array)—— 原型链:[] → Array.prototype → Object.prototype → null
├── 函数(Function)—— 原型链:function → Function.prototype → Object.prototype → null
├── 正则(RegExp)
├── 日期(Date)
├── 包装对象(String、Number、Boolean)
├── Map、Set、WeakMap、WeakSet(ES6)
└── Promise、Proxy、Reflect 等(ES6+)
所有对象类型最终都通过原型链连接到 Object.prototype,而 Object.prototype.__proto__ 是 null。这就是原型链的终点。
八、本篇小结
这一篇我们从 JavaScript 的历史出发,深入了它的面向对象机制:
- JavaScript 在 10 天内被创造,采用了基于原型的面向对象设计,与主流类继承语言有本质区别。
- 原型链是属性查找的路径:对象自身 →
__proto__→ 原型的__proto__→ … →Object.prototype→null。 __proto__vsprototype:前者是每个对象都有的原型链接(内部槽[[Prototype]]),后者是函数才有的属性,用来给通过new创建的实例设置原型。- “一切皆对象”不准确:七种基本类型不是对象。它们可以临时被包装成对象来调用方法。
new的四步:创建空对象 → 绑定原型 → 执行构造函数 → 返回对象。class是语法糖:底层仍然是原型继承。用class写代码更清晰,但理解原型链才能调试深层问题。this是动态绑定:取决于函数如何被调用,而不是在哪里定义。四种调用方式(普通、方法、构造函数、显式绑定)决定了this的值。
理解了原型链,你就理解了 JavaScript 对象模型的内核。下一篇,我们将深入 JavaScript 的数据类型系统,剖析 typeof、instanceof 的原理,以及基本类型和引用类型在内存中的本质区别。
下一篇预告
下一篇——《JavaScript 数据类型》:typeof 为什么对 null 返回 'object'?instanceof 的内部机制是什么?基本类型和引用类型在内存中如何存储?为什么 0.1 + 0.2 !== 0.3?这些问题,下一篇一一拆解。
前端,每周更新。













暂无评论内容