一、原型方法与对象方法优先级
js
let a = {
show() {
console.log("对象方法!");
},
};
a.__proto__.show = function () {
console.log("原型方法!");
};
a.show(); // 输出:对象方法!
/*
执行对象方法时,
先查看对象有无该方法,
有的话直接使用对象方法,没有的话再沿着原型链逐步向上查找。
*/
js
function Abc() {}
Abc.prototype.show = function () {
console.log("protoType show");
};
let abc = new Abc();
abc.__proto__.show = function () {
console.log("__proto__ show");
};
abc.show(); // 输出:protoType show。
/*
function函数比较特别。
使用new创建对象后,调用方法时,
会先在函数的prototype对象上查找,未找到再沿着原型链去找。
*/
二、指定对象的原型
js
Object.setPrototypeOf(obj, prototype);
/*
obj:
要设置其原型的对象。
prototype:
该对象的新原型(一个对象或null)。
返回值:
指定的对象。
*/
三、"in" vs "hasOwnProperty()"
js
/*
1、"in"检测某一属性、方法等是否存在于对象或对象的原型链上。
2、"hasOwnProperty()"仅检测某一属性、方法等是否存在于对象上,不检测对象的原型链。
*/
let a = {
name: "a",
age: 18,
sex: "male",
hobby: ["football", "basketball"],
info: {
city: "beijing",
country: "china",
},
getName() {
return this.name;
},
};
let b = {
address: "beijing",
};
Object.setPrototypeOf(b, a);
console.log("address" in b); // true。
console.log("name" in b); // true。
console.log("age" in b); // true。
console.log("sex" in b); // true。
console.log("hobby" in b); // true。
console.log("info" in b); // true。
console.log("city" in b); // false。
console.log("country" in b); // false。
console.log("getName" in b); // true。
console.log(b.hasOwnProperty("address")); // true。
console.log(b.hasOwnProperty("name")); // false。
console.log(b.hasOwnProperty("age")); // false。
console.log(b.hasOwnProperty("sex")); // false。
console.log(b.hasOwnProperty("hobby")); // false。
console.log(b.hasOwnProperty("info")); // false。
console.log(b.hasOwnProperty("city")); // false。
console.log(b.hasOwnProperty("country")); // false。
console.log(b.hasOwnProperty("getName")); // false。
// 所以遍历对象时,尽量使用"hasOwnProperty()"进行判断。
for (let key in b) {
if (b.hasOwnProperty(key)) {
console.log("own property: " + key); // 仅输出:own property: address。
}
console.log(key);
/*
输出:
address
name
age
sex
hobby
info
getName
*/
}
四、继承(ES5)
js
// Shape------父类。
function Shape() {
this.x = 0;
this.y = 0;
}
// 父类方法。
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info("Shape moved.");
};
// Rectangle------子类。
function Rectangle() {
Shape.call(this); // 调用父类构造函数。
}
// 子类继承父类。方式一(推荐):
/*
注意:
不可以直接写"Rectangle.prototype = Shape.prototype"。
因为这样写的话,表示"Rectangle.prototype"和"Shape.prototype"都指向同一个对象(Shape.prototype)。
一旦"Rectangle.prototype"被修改,"Shape.prototype"也会被修改。
比如要给"Shape.prototype"添加新的属性或方法时,也会同时添加给"Rectangle.prototype"。
所以在ES5中构造方法实现继承时,
1、创建一个对象"obj",将"obj"赋给"Rectangle.prototype"。
2、将"obj"的原型引用指向"Shape.prototype",
这样"Rectangle"创建的对象就可以通过原型引用(__proto__)查找其原型"obj"的原型,
进而可以使用"Shape.prototype"的属性和方法,从而实现继承。
3、添加"Rectangle.prototype"的构造器(constructor)到"obj"中,
避免通过"Rectangle"创建的对象使用的是继承的"Shape"的原型中的构造器,
因为将"obj"赋值给"Rectangle.prototype"后,"Rectangle"自身的构造器就没了(obj中没有Rectangle的构造器)。
所以此时我们需要手动添加"Rectangle"的构造器到"obj"中。
如下所示:
*/
Rectangle.prototype = Object.create(Shape.prototype, {
/*
如果不将"Rectangle.prototype.constructor"设置为"Rectangle",
它将采用"Shape"(父类)的"prototype.constructor"。
为避免这种情况,我们将"prototype.constructor"设置为"Rectangle"(子类)。
*/
constructor: {
value: Rectangle,
enumerable: false,
writable: true,
configurable: true,
},
});
// 子类继承父类。方式二(不推荐):
Rectangle.prototype.__proto__ = Shape.prototype;
// 子类继承父类。方式三(不推荐):
Object.setPrototypeOf(Rectangle.prototype, Shape.prototype);
const rect = new Rectangle();
console.log("rect 是 Rectangle 类的实例吗?", rect instanceof Rectangle); // true。
console.log("rect 是 Shape 类的实例吗?", rect instanceof Shape); // true。
rect.move(1, 1); // 输出:'Shape moved.'。
上述三种方式子类继承父类的代码目的相同(让Rectangle.prototype继承自Shape.prototype),但它们在实现机制、副作用、性能和规范性上存在显著差异。
三者详细对比:
-
Rectangle.prototype = Object.create(Shape.prototype, { constructor: ... })。推荐方式(ES5标准做法)。
原理:
-
创建一个全新的对象 ,其
[[Prototype]]指向Shape.prototype。 -
将这个新对象赋值给
Rectangle.prototype。 -
显式定义
constructor属性为Rectangle,并设为不可枚举(符合内置行为)。
优点:
-
语义清晰 :明确表达"
Rectangle.prototype是一个以Shape.prototype为原型的新对象"。 -
无副作用 :不修改任何现有对象,只是替换
prototype引用。 -
标准兼容:ES5+正式API,所有环境支持。
-
可精确控制属性描述符 (如
enumerable: false)。
注意:
-
原来的
Rectangle.prototype(由构造函数自动创建的那个{ constructor: Rectangle })会被丢弃。 -
所有原本定义在旧
prototype上的方法必须重新定义在新对象上(通常在设置完继承后再添加)。示例:
jsfunction Shape() {} Shape.prototype.move = function() { console.log('move'); }; function Rectangle() {} // 设置继承。 Rectangle.prototype = Object.create(Shape.prototype, { constructor: { value: Rectangle, enumerable: false } }); // 添加自身方法。 Rectangle.prototype.area = function() { return this.w * this.h; };
-
-
Rectangle.prototype.__proto__ = Shape.prototype。不推荐(非标准、已废弃)。
原理:
-
直接修改现有
Rectangle.prototype对象的内部[[Prototype]]链。 -
不创建新对象,而是在原对象上"打补丁"。
缺点:
-
__proto__是非标准属性 (虽被ES6 Annex B收录,但仅用于Web兼容,不建议在生产代码中使用)。 -
破坏封装性:动态修改对象原型链,使引擎难以优化(V8会deoptimize)。
-
可能引发意外行为 :如果之前已在
Rectangle.prototype上定义了方法,虽然保留,但语义混乱。 -
在严格模式或某些JS引擎中可能被禁用。
-
无法干净控制
constructor属性的行为(虽然它还在,但容易被忽略)。
本质:
- 这是对已有对象的运行时篡改,而非声明式构建。
-
-
Object.setPrototypeOf(Rectangle.prototype, Shape.prototype)。可用但不推荐(性能差)。
原理:
-
与
__proto__效果完全相同:修改现有对象的原型链。 -
但使用的是ES6标准API (
Object.setPrototypeOf是正式方法)。
优点:
-
是标准方法,比
__proto__更"合法"。 -
语义明确:"设置某对象的原型"。
缺点:
-
仍然修改现有对象的原型链,而非创建新对象。
-
性能极差 :现代JS引擎(如V8)会对动态修改
[[Prototype]]的对象进行严重去优化。 -
TypeScript官方文档、MDN、Google JS Style Guide均明确建议避免使用。
-
同样存在
constructor管理不清晰的问题。
-
三者核心区别总结:
| 特性 | Object.create(...) |
prototype.__proto__ = ... |
Object.setPrototypeOf(...) |
|---|---|---|---|
| 是否创建新对象 | 是。 | 否(修改原对象)。 | 否(修改原对象)。 |
| 是否标准 | ES5+。 | 非标准(遗留)。 | ES6标准。 |
| 性能 | 快(静态结构)。 | 慢(动态修改)。 | 慢(动态修改)。 |
| 可维护性 | 高(声明式)。 | 低(隐式修改)。 | 低(副作用)。 |
constructor控制 |
精确。 | 依赖原有。 | 依赖原有。 |
| 推荐度 | 强烈推荐。 | 禁止使用。 | 仅用于特殊场景(如polyfill)。 |
为什么Object.create是最佳选择?
因为它遵循了"组合优于修改"和"不可变思维":
-
不动原始对象。
-
创建一个干净的新原型链。
-
结构清晰,易于理解和调试。
-
与现代JS(包括
class extends内部实现)理念一致。
实际上,class Rectangle extends Shape {}在底层就是用类似Object.create(Shape.prototype)的方式设置原型的。
总结:
| 写法 | 是否应该用? | 原因 |
|---|---|---|
Object.create(...) |
应该用 | 标准、高效、清晰。 |
prototype.__proto__ = ... |
不要用 | 非标准、危险、过时。 |
Object.setPrototypeOf(...) |
避免用 | 标准但性能差,仅限不得已场景。 |
记住:继承的本质是"建立原型链",而不是"修改已有对象的原型"。所以,创建新对象,胜过修改旧对象。
五、Mixin实现"多继承"
js
function Address() {}
Address.prototype.getAddress = function () {
console.log("这是地址!");
};
function Credit() {}
Credit.prototype.total = function () {
console.log("这是积分统计!");
};
function Request() {}
Request.prototype.ajax = function () {
console.log("这是请求后台!");
};
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.show = function () {
console.log(this.name, this.age);
};
/*
此时"User"实例要想实现"Address"、"Credit"、"Request"功能,
可让"User"继承"Request","Request"继承"Credit","Credit"继承"Address"。
但这样多继承,会造成混乱。
*/
使用Mixin(混合功能):
js
// 第一步:将"Request"、Credit"、Address"全部变为对象。
const Address = {
getAddress() {
console.log("这是地址!");
},
setAddress() {
console.log("这是设置地址!");
},
};
const Credit = {
total() {
console.log("这是积分统计!");
},
};
const Request = {
ajax() {
console.log("这是请求后台!");
},
};
// 第二步:将上述对象中的方法赋值给"User"原型。
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.show = function () {
console.log(this.name, this.age);
};
/*
User.prototype.getAddress = Address.getAddress;
User.prototype.setAddress = Address.setAddress;
User.prototype.total = Credit.total;
User.prototype.ajax = Request.ajax
*/
// 若要赋值给"User"原型的方法过多,可使用Object.assign()方法。
User.prototype = Object.assign(User.prototype, Address, Credit, Request);
let user = new User("张三", 18);
user.getAddress();
user.setAddress();
user.total();
user.ajax();
user.show();
六、super
js
const Base = {
getInfo() {
return "这是Base!";
},
};
const Address = {
// "__proto__: Base,"方法不推荐,建议使用如下"Object.setPrototypeOf()"方法实现对象继承。
getInfo() {
return "这是Address!";
},
};
Object.setPrototypeOf(Address, Base);
const Credit = {
total() {
console.log(this.getInfo() + "+这是积分统计!"); // 用"super.getInfo()",原因如下。
},
};
Object.setPrototypeOf(Credit, Address);
function User(name, age) {
this.name = name;
this.age = age;
}
// 将Credit对象的自有属性复制到User.prototype,而不会复制Credit对象的原型链。
User.prototype = Object.assign(User.prototype, Credit);
let user = new User("张三", 18);
Credit.total(); // 正确。
/*
上述"total()"方法中使用"this.getInfo()"或"super.getInfo()"时,
输出都是:这是Address!+这是积分统计!
因为"Credit"原型引用指向的"Address"对象中有"getInfo()"方法,
所以JS不会继续往上找"Address"原型引用指向的"Base"对象中的"getInfo()"方法。
*/
user.total(); // 报错:TypeError: this.getInfo is not a function。
/*
"Credit.total();"
正确是因为JS在Credit对象中没找到"this.getInfo()"方法时(this指向Credit对象),
会去Credit对象的原型链中查找。
"user.total();"
错误是因为JS在user对象中没找到"this.getInfo()"方法时(this指向user对象),
会去user对象原型链中查找。
而user对象原型链中找不到"this.getInfo()"方法,所以报错。
解决办法:
将"this.getInfo()"改为"super.getInfo()"。
*/
js
const Base = {
getInfo1() {
return "这是Base!";
},
};
const Address = {
getInfo2() {
return this.getInfo1();
},
};
Object.setPrototypeOf(Address, Base);
const Credit = {
total() {
console.log(this.getInfo2() + "+这是积分统计!");
},
};
Object.setPrototypeOf(Credit, Address);
function User(name, age) {
this.name = name;
this.age = age;
}
Object.assign(User.prototype, Credit);
let user = new User("张三", 18);
Credit.total(); // 正确。
/*
上述"total()"方法中"this.getInfo2()"、"getInfo2()"方法中"this.getInfo1()",
上述使用"this"或将"this"全部改为"super"。
输出都是:这是Base!+这是积分统计!
因为使用"this"调用方法时,若对象中没有,则会去对象原型链中查找。
而使用"super"时,则表示直接去对象的原型中查找。
上述"this.getInfo2()"、"this.getInfo1()"中的this全部指向Credit对象。
*/
user.total(); // 报错:TypeError: this.getInfo2 is not a function。
/*
上述"this.getInfo2()"、"this.getInfo1()"中的this全部指向user对象。
解决办法:
将"this.getInfo2()"、"this.getInfo1()"中的this全部改为"super"。
*/
/*
1、"super"的核心规则。
"super"在对象方法中的指向由方法的"[[HomeObject]]"属性决定:
1、方法定义在哪个对象中,该对象就是方法的"[[HomeObject]]"。
2、"super"始终指向"[[HomeObject]]"的原型对象。
3、即使方法被复制到其他对象,其"[[HomeObject]]"和"super"指向也不会改变。
"[[HomeObject]]"介绍:
"[[HomeObject]]"是ES6引入的一个内部槽(internal slot),用于支持"super"的静态绑定。
它的作用是:记录这个方法"属于哪个对象"。
"super.method();":
1、获取当前类的父类的原型(即"Object.getPrototypeOf(Child.prototype) → Parent.prototype")。
2、在这个原型对象上查找"method"。
3、如果没找到,就继续沿着"Parent.prototype"的"[[Prototype]]"向上找(即"GrandParent.prototype",依此类推)。
4、这和调用"this.method()"的查找路径几乎一样,只是起点不同:
1、this.method():从当前实例的原型(Child.prototype)开始找。
2、super.method():从父类的原型(Parent.prototype)开始找。
2、上述方法中"super"分析。
"getInfo2()"方法定义在"Address"对象中,因此"[[HomeObject]] = Address"。
"Address"的原型是"Base"(通过"Object.setPrototypeOf(Address, Base)"设置)。
所以"super.getInfo1()"中的"super"始终指向"Base"对象
"total()"方法定义在"Credit"对象中,因此"[[HomeObject]] = Credit"。
"Credit"的原型是"Address"(通过"Object.setPrototypeOf(Credit, Address)"设置)。
所以"super.getInfo2()"中的"super"始终指向"Address"对象。
3、方法复制后的"super"指向。
当通过"Object.assign(User.prototype, Credit)"将"Credit.total()"复制到"User.prototype"后,
虽然"total()"方法现在存在于"User.prototype"上,但它的"[[HomeObject]]"仍然是"Credit"(定义时所在的对象)。
因此"super.getInfo2()"中的"super"仍然指向"Address"对象,而不是"User.prototype"的原型。
这就是为什么即使方法被复制,"super"的指向也不会改变。
4、与"this"的本质区别。
"this"是动态的:指向调用该方法的对象,取决于调用方式。
"super"是静态的:指向方法定义时所在对象的原型,不会随调用方式改变。
*/
super是JavaScript中用于访问和调用父类(超类)的属性和方法 的关键字。它在类继承(class extends)场景中非常关键,尤其在子类的构造函数或方法中。
super的两种主要用法:
-
作为函数调用:
super()。-
只能在子类的
constructor中使用。 -
作用:调用父类的构造函数,完成父类的初始化。
-
必须在访问
this之前调用(否则报错)。jsclass Animal { constructor(name) { this.name = name; } } class Dog extends Animal { constructor(name, breed) { super(name); // 调用父类构造函数,初始化this.name。 this.breed = breed; // 必须在super()之后才能用this。 } } const dog = new Dog("Buddy", "Golden Retriever"); console.log(dog.name); // "Buddy"。 console.log(dog.breed); // "Golden Retriever"。错误示例:
jsclass Dog extends Animal { constructor(name) { this.name = name; // ReferenceError: Must call super() before accessing 'this'。 super(name); } }
-
-
作为对象使用:
super.method()或super.property。-
可以在子类的任何方法中使用(包括普通方法、静态方法)。
-
用于调用父类的同名方法或访问父类属性。
普通方法中使用:
jsclass Animal { speak() { return `makes a noise.`; } } class Dog extends Animal { speak() { // 调用父类的speak()。 return super.speak() + " Woof!"; } } const dog = new Dog(); console.log(dog.speak()); // "makes a noise. Woof!"。静态方法中使用:
jsclass Parent { static hello() { return "Hello from Parent"; } } class Child extends Parent { static hello() { return super.hello() + " and Child!"; } } console.log(Child.hello()); // "Hello from Parent and Child!" /* 静态方法不是属于类本身(类是构造函数的语法糖。构造函数也是对象,静态方法是构造函数对象的属性,不在其prototype上)吗? 按照super的核心规则,它指向的应该是构造函数的"__proto__",即Function才对。为什么会指向父类? 详情请参考第五章(类)第5.1节(继承(类 vs 构造函数))。 */ -
super在对象字面量中的用法(ES2015+):
-
super也可以在对象的方法简写中使用(但很少见)。jsconst parent = { greet() { return "Hi"; } }; const child = { __proto__: parent, greet() { return super.greet() + " there!"; // 合法。 } }; console.log(child.greet()); // "Hi there!"注意:必须使用 方法简写语法 (
greet() {}),不能用greet: function() {}。
在JavaScript中,super的指向需要根据方法类型 和上下文区分。
核心规则是:super指向方法[[HomeObject]]的原型 ,而不是方法自身的prototype(如果有的话)。
核心概念:
super的指向由方法的内部[[HomeObject]]属性决定:
-
只有对象方法(使用简写语法) 、类方法 、类构造函数 才有
[[HomeObject]]。 -
[[HomeObject]]是方法定义时所在的对象(固定不变)。 -
super始终指向Object.getPrototypeOf([[HomeObject]])。
不同场景下super的指向:
-
对象字面量方法中的
super。jsconst Parent = { x: 1 }; const Child = { method() { return super.x; // super指向谁? } }; Object.setPrototypeOf(Child, Parent); Child.method(); // 输出: 1。-
method的[[HomeObject]]=Child(方法定义在Child中)。 -
super指向Object.getPrototypeOf(Child),即Parent。 -
这里
Child的__proto__就是Parent,所以super指向Child.__proto__。
-
-
类方法中的
super。jsclass Parent { parentMethod() { return "parent"; } } class Child extends Parent { childMethod() { return super.parentMethod(); // super指向谁? } }-
childMethod的[[HomeObject]]=Child.prototype(类方法默认挂载在类的prototype上)。 -
super指向Object.getPrototypeOf(Child.prototype),即Parent.prototype。 -
这里
Child.prototype.__proto__=Parent.prototype,所以super指向Child.prototype.__proto__。
-
-
类构造函数中的
super(特殊情况)。jsclass Parent { constructor(name) { this.name = name; } } class Child extends Parent { constructor(name, age) { super(name); // super指向谁? this.age = age; } }-
类构造函数中的
super()是特殊语法,用于调用父类构造函数。 -
此时
super指向父类构造函数本身 (Parent构造函数),而不是原型。 -
执行
super(name)等价于Parent.call(this, name),用于初始化父类属性。
-
关键区分: super与prototype/__proto__的关系。
| 概念 | 指向 | 作用 |
|---|---|---|
super(对象/类方法) |
[[HomeObject]]的原型(即Object.getPrototypeOf([[HomeObject]]))。 |
访问父级原型上的属性/方法。 |
super(类构造函数) |
父类构造函数本身。 | 调用父类构造函数。 |
对象的__proto__ |
构造函数的prototype。 |
建立对象的原型链。 |
函数的prototype |
函数的原型对象。 | 用于new操作时建立实例的原型链。 |
常见误区:
-
普通函数中的
super。普通函数没有
[[HomeObject]],不能使用super,会直接报错。jsfunction foo() { super.x; // 语法错误:'super' keyword unexpected here。 } -
方法复制后
super的指向。即使方法被复制到其他对象,
[[HomeObject]]和super指向不变。jsconst Parent = { x: 1 }; const Child = { method() { return super.x; } }; Object.setPrototypeOf(Child, Parent); const Other = {}; Other.method = Child.method; // 复制方法。 Other.method(); // 输出: 1(super仍指向Parent,不是Other的原型)。