各位观众老爷们,大家好!今天咱们不聊风花雪月,来聊聊 JavaScript 这位老朋友新穿的“糖果色外套”—— Class
语法。
别看 Class
关键字让 JavaScript 看起来像个正经的面向对象语言了,但扒开这层糖衣,你会发现里面还是那个熟悉的 Prototype Chain
(原型链)在默默支撑。 咱们还要顺便看看 new.target
这个小家伙,它在 Class
的世界里扮演着什么角色。
第一幕:Class
登场,糖衣炮弹来袭
JavaScript 早期并没有 Class
关键字,创建对象和实现继承的方式略显繁琐。比如下面这样:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
function Student(name, major) {
Person.call(this, name); // 借用构造函数继承属性
this.major = major;
}
Student.prototype = Object.create(Person.prototype); // 关键:原型链继承
Student.prototype.constructor = Student; // 修正 constructor 指向
Student.prototype.study = function() {
console.log(`${this.name} is studying ${this.major}`);
};
const john = new Student("John", "Computer Science");
john.sayHello(); // Hello, my name is John
john.study(); // John is studying Computer Science
看到没? 为了实现继承,又是 call
,又是 Object.create
,又是修正 constructor
,简直像在做一套广播体操,步骤多到让人怀疑人生。
然后,ES6 带来了 Class
语法,让代码瞬间变得优雅起来:
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
class Student extends Person {
constructor(name, major) {
super(name); // 调用父类的 constructor
this.major = major;
}
study() {
console.log(`${this.name} is studying ${this.major}`);
}
}
const jane = new Student("Jane", "Biology");
jane.sayHello(); // Hello, my name is Jane
jane.study(); // Jane is studying Biology
这简洁的代码,这清晰的结构,是不是感觉世界都美好了? extends
关键字一出,继承关系一目了然,super
关键字也让调用父类方法变得轻松愉快。
但是! 别被表象迷惑了! Class
仅仅是 JavaScript 原型继承机制的语法糖。它并没有改变 JavaScript 的继承本质,只是让代码更容易阅读和编写。
第二幕:原型链的秘密,挖地三尺也要找到你
要理解 Class
背后的原型链,我们先要搞清楚几个概念:
- 构造函数 (Constructor): 用
new
关键字调用的函数,用于创建对象。 - 原型对象 (Prototype): 每个函数都有一个
prototype
属性,它指向一个对象。这个对象就是通过该函数创建的实例的原型。 - 实例对象 (Instance): 通过
new
关键字创建的对象。 __proto__
属性: 每个对象(除了null
)都有一个__proto__
属性,指向创建该对象的构造函数的原型对象。
有了这些概念,我们就可以画出原型链的关系图:
实例对象 --> __proto__ --> 构造函数的原型对象 --> __proto__ --> Object.prototype --> __proto__ --> null
简单来说,当我们访问一个对象的属性或方法时,JavaScript 引擎会按照以下顺序查找:
- 对象自身是否有该属性或方法。
- 如果没有,就沿着
__proto__
属性指向的原型对象查找。 - 如果原型对象也没有,就继续沿着原型对象的
__proto__
属性向上查找,直到找到Object.prototype
。 - 如果
Object.prototype
也没有,就沿着Object.prototype.__proto__
查找到null
,查找结束,返回undefined
。
现在,让我们回到 Class
语法。 Class
声明实际上做了什么?
class MyClass {
constructor(value) {
this.value = value;
}
getValue() {
return this.value;
}
}
console.log(typeof MyClass); // function
console.log(MyClass.prototype.constructor === MyClass); // true
console.log(MyClass.prototype); // {constructor: f, getValue: f, __proto__: Object}
从上面的代码可以看出:
Class
声明本质上还是创建了一个函数。Class
的prototype
属性指向一个对象,该对象包含了Class
中定义的方法。Class
的prototype
属性的constructor
属性指向Class
本身。
也就是说,Class
只是把构造函数和原型对象的操作封装起来了,让代码更易读。 extends
关键字也只是简化了原型链继承的写法。
让我们再看一个 extends
的例子,并用原型链的视角来分析:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // Buddy barks.
这段代码背后发生了什么?
Dog extends Animal
实际上是设置了Dog.prototype.__proto__ = Animal.prototype
。super(name)
调用了父类Animal
的构造函数,相当于Animal.call(this, name)
。Dog
类中的speak
方法覆盖了Animal
类中的speak
方法(方法重写)。
我们可以用下面的表格来总结 Class
语法和原型链的关系:
Class 语法 |
原型链实现 |
---|---|
class MyClass {} |
function MyClass() {} |
constructor() |
构造函数 |
method() |
MyClass.prototype.method = function() {} |
extends ParentClass |
MyClass.prototype.__proto__ = ParentClass.prototype |
super() |
ParentClass.call(this) |
第三幕:new.target
的逆袭,谁在呼唤我?
new.target
是一个 ES6 新增的属性,它指向使用 new
关键字调用的构造函数或 Class
。 简单来说,它告诉你“我是谁用 new
调用的”。
new.target
主要有以下几个用途:
-
判断函数是否被
new
调用: 如果函数不是通过new
关键字调用的,new.target
的值为undefined
。function MyFunction() { if (!new.target) { throw new Error("MyFunction must be called with new"); } this.value = 10; } const obj = new MyFunction(); // 正确 //MyFunction(); // 抛出错误
-
在抽象类中防止直接实例化: 抽象类通常只用于继承,不应该被直接实例化。我们可以使用
new.target
来防止这种情况。class AbstractClass { constructor() { if (new.target === AbstractClass) { throw new Error("AbstractClass cannot be instantiated directly"); } } abstractMethod() { throw new Error("Abstract method must be implemented in subclass"); } } class ConcreteClass extends AbstractClass { abstractMethod() { console.log("Implemented method"); } } //const abstractObj = new AbstractClass(); // 抛出错误 const concreteObj = new ConcreteClass(); // 正确 concreteObj.abstractMethod(); // Implemented method
-
确定继承链中的哪个类被
new
调用: 在继承链中,new.target
指向的是直接被new
调用的类,而不是父类。class Base { constructor() { console.log("Base constructor", new.target.name); } } class Derived extends Base { constructor() { super(); console.log("Derived constructor", new.target.name); } } const baseObj = new Base(); // Base constructor Base const derivedObj = new Derived(); // Base constructor Derived Derived constructor Derived
在这个例子中,即使
Base
类的构造函数被Derived
类调用,new.target
仍然指向Derived
类。
new.target
的作用在于提供了更灵活的控制,让我们可以在运行时判断函数的调用方式和类的实例化行为。 它就像一个“身份识别器”,帮助我们更好地理解和管理 JavaScript 的面向对象编程。
第四幕:总结与升华,拨开云雾见青天
总而言之,Class
语法是 JavaScript 原型继承机制的语法糖。 它让代码更易读、易写,但并没有改变 JavaScript 的继承本质。 理解原型链是理解 Class
语法的关键。
new.target
则是一个强大的工具,可以帮助我们判断函数的调用方式和类的实例化行为,从而实现更灵活的控制。
希望今天的讲座能够帮助大家更好地理解 JavaScript 的 Class
语法和原型链机制。记住,不要被表面的糖衣迷惑,要深入理解背后的原理,才能真正掌握 JavaScript 这门语言。
下课!