原型继承
原型继承
特殊属性 [[Prototype]]
此属性只能有两个值:
null- 另一个对象引用,称为原型
当使用被调用对象中不存在的属性或者方法时,会自动从原型上进行查找,此外,原型链支持长链,对象A可以是对象B的原型,B对象可以是对象C的原型,以此类推。
但是,原型对象只能有一个,一个对象不允许存在多个原型对象。(理解为单继承)
[[Prototype]]是内部隐藏的属性,但可以通过__proto__进行显式设置。
外部进行调用设置:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal
或者,创建对象时进行显式指定:
let animal = {
eats: true,
walk() {
console.info("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// walk 是通过原型链获得的
longEar.walk(); // Animal walk
console.info(longEar.jumps); // true(从 rabbit)
原型链设置的限制:
- 禁止形成闭环引用
__proto__只能是对象或者null,其余类型都会忽略
__proto__ 与 [[Prototype]]
__proto__ 是 [[Prototype]] 的 getter/setter。
但是通过__proto__来设置原型对象属于过时的,但为保持兼容性依旧保留,
现代编程推荐使用:
// 获取原型
Object.getPrototypeOf
// 设置原型
Object.setPrototypeOf
示例
let obj = { name: 'John' };
// 使用 __proto__ 获取原型
console.log(obj.__proto__); // [Object: null prototype] {}
// 使用 __proto__ 设置原型
let prototype = { age: 30 };
obj.__proto__ = prototype;
console.log(obj.age); // 30
// 现代方法获取原型
console.log(Object.getPrototypeOf(obj)); // { age: 30 }
// 现代方法设置原型
Object.setPrototypeOf(obj, { city: 'New York' });
console.log(obj.city); // New York
在规范要求中,
__proto__仅受浏览器环境支持,但实际所有JavaScript环境都支持。
原型仅用于读取
对于所有的写入/删除操作是直接在对象上进行,
即使是原型已有的属性,也只会在当前对象进行操作。
let animal = {
eats: true,
walk() {
/* rabbit 不会使用此方法 */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
console.info("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
此外,使用for in也能够迭代出继承的属性。
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys 只返回自己的 key
console.info(Object.keys(rabbit)); // jumps
// for..in 会遍历自己以及继承的键
for(let prop in rabbit) console.info(prop); // jumps,然后是 eats
但依旧可以通过obj.hasOwnProperty(key)判断出哪些是继承属性,哪些是自有属性:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
console.info(`Our: ${prop}`); // Our: jumps
} else {
console.info(`Inherited: ${prop}`); // Inherited: eats
}
}
rabbit.hasOwnProperty(prop)从何而来?
继承自Object,所有对象都是继承自Object,但属于不可迭代属性,不会进行展示。
访问器属性 this
访问器属性的赋值是由setter函数处理的,本质与调用函数一致。
因此this可以动态的进行计算,能够准确的获取当前被读写的对象。
无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,
this始终是点符号.前面的对象。
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
console.info(admin.fullName); // John Smith (*)
// setter triggers!
admin.fullName = "Alice Cooper"; // (**)
console.info(admin.fullName); // Alice Cooper,admin 的内容被修改了
console.info(user.fullName); // John Smith,user 的内容被保护了
F.prototype
prototype
这里的prototype并非内建属性,在以前尚不存在对原型进行直接访问的方式,
因此手动添加一个名为prototype的属性。
如果F.prototype是一个对象,那么new操作符会使用它为新对象设置[[Prototype]]。
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
// “当创建了一个 new Rabbit 时,
// 把它的 [[Prototype]] 赋值为 animal”。
Rabbit.prototype = animal;
let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
console.info( rabbit.eats ); // true
要注意:
此时rabbit实例是继承自animal,但Rabbit对象并不是。
如果将prototype属性更改为其他对象,那么后续创建的新实例将以新对象为原型。
已有的实例原型不会改变。
默认的prototype与构造器属性
每个函数都有"prototype"属性,即使我们没有提供它
function Rabbit() {}
/* 默认的 prototype
Rabbit.prototype = { constructor: Rabbit };
*/
console.info( Rabbit.prototype.constructor == Rabbit ); // true
同样,对于生成的对象实例:
function Rabbit() {}
let rabbit = new Rabbit(); // 继承自 {constructor: Rabbit}
console.info(rabbit.constructor == Rabbit); // true (from prototype)
因此可以通过已创建的实例来创建一个新实例
这种方法非常适合创建一个来自第三方库,但不清楚其具体的构造器时,采取此方法很方便。
function Rabbit(name) {
this.name = name;
console.info(name);
}
let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");
构造器是可修改的,甚至覆盖
function Rabbit() {}
Rabbit.prototype = {
jumps: true
};
let rabbit = new Rabbit();
console.info(rabbit.constructor === Rabbit); // false
因此建议选择添加/删除属性到默认"prototype",而不是将其整个覆盖:
function Rabbit() {}
Rabbit.prototype.jumps = true
或者类似于__proto__的显式指定
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
原生的原型
Object.prototype
let obj = {};
// 等同于
let obj = new Object();
Object是一个内建的构造函数,其自身的prototype指向一个包含其他方法的巨大对象。
因此普通的obj自创建就已有很多内建方法。
一切的对象都是继承自Object,Object的prototype上方无原型,指向null。
let obj = {};
console.info(obj.__proto__ === Object.prototype); // true
console.info(obj.toString === obj.__proto__.toString); //true
console.info(obj.toString === Object.prototype.toString); //true
其余内建原型
除了Object,对于Array、Date、Function等都在prototype上挂载了属于自己的方法。
但是所有内建对象的原型顶端都是Object.prototype。因此一切都是继承自对象。
let arr = [1, 2, 3];
// 它继承自 Array.prototype?
console.info( arr.__proto__ === Array.prototype ); // true
// 接下来继承自 Object.prototype?
console.info( arr.__proto__.__proto__ === Object.prototype ); // true
// 原型链的顶端为 null。
console.info( arr.__proto__.__proto__.__proto__ ); // null
对于一些重叠的方法,依据最近原则,调用在原型链上距离最近的方法进行运行。
基本数据类型
对于数字、字符串以及布尔值情况下,由于并非是对象,当访问他们的属性时,其实调用的是相应的构造器。
这些构造器提供了相应的方法进行调用,使用完后消失。
可以通过String.prototype、Number.prototype和Boolean.prototype进行获取。
null与undefined无对象包装器,因此不存在方法和属性,也不存在相应的原型。
原型可修改
原型并非只是只读属性,允许进行手动添加
String.prototype.show = function() {
console.info(this);
};
"BOOM!".show(); // BOOM!
但通常不建议如此使用,有可能会造成命名冲突,导致功能函数被覆盖。
不过在需要的情况下可以通过polyfill进行填充处理。
if (!String.prototype.repeat) { // 如果这儿没有这个方法
// 那就在 prototype 中添加它
String.prototype.repeat = function(n) {
// 重复传入的字符串 n 次
// 实际上,实现代码比这个要复杂一些(完整的方法可以在规范中找到)
// 但即使是不够完美的 polyfill 也常常被认为是足够好的
return new Array(n + 1).join(this);
};
}
console.info( "La".repeat(3) ); // LaLaLa
方法借用
前文提过,可以借助其他对象的内建方法在另一个类型对象上使用,只要满足其所需要的规则。
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
console.info( obj.join(',') ); // Hello,world!
因为内建的方法
join的内部算法只关心正确的索引和length属性。它不会检查这个对象是否是真正的数组。
原型方法,无原型对象
指定原型创建对象
方法签名:
// 利用给定的 proto 作为 [[Prototype]] 和可选的属性描述来创建一个空对象。
Object.create(proto, [descriptors])
示例:
let animal = {
eats: true
};
// 创建一个以 animal 为原型的新对象
let rabbit = Object.create(animal); // 与 {__proto__: animal} 相同
console.info(rabbit.eats); // true
console.info(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // 将 rabbit 的原型修改为 {}
也可以在创建对象时提供额外的属性:
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
全新的对象克隆方式:
let clone = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
真正准确地拷贝,包括所有的属性:可枚举和不可枚举的,数据属性和 setters/getters,
包括所有内容,并带有正确的 [[Prototype]]
原型简史
为什么 __proto__ 被部分认可并允许在 {...} 中使用,但仍不能用作 getter/setter?
__proto__ 是一个历史遗留的属性,用于访问和修改对象的 [[Prototype]]。尽管它在实际开发中被广泛支持,但它并不是标准的一部分。2022年,官方允许在对象字面量 {...} 中使用 __proto__,但不允许用作 getter/setter(即 obj.__proto__)。这是因为:
- 历史兼容性:
__proto__已经在几乎所有环境中实现,完全移除它会导致大量现有代码无法运行。 - 规范限制:标准方法
Object.getPrototypeOf和Object.setPrototypeOf更为规范和安全,因此推荐使用这些方法来操作原型。 - 性能问题:直接修改
__proto__会导致性能下降,因为它会破坏对象属性访问的内部优化。
纯空对象
在 JavaScript 中,对象可以作为关联数组使用,但存在一个潜在问题:__proto__ 是一个特殊的属性,可能会导致意外行为。为了避免这些问题,可以使用 Object.create(null) 创建一个没有原型的“纯空对象”:
let obj = Object.create(null);
let key = "__proto__";
obj[key] = "some value";
console.info(obj[key]); // "some value"
如果不是纯空对象,则会显示为
[object Object]
这种对象被称为“very plain”或“pure dictionary”对象,因为它们比普通对象更简单,没有继承任何方法。缺点是它们没有内建的 toString 等方法,但可以通过 Object.keys 等标准方法操作。
修改已存在的对象的 [[Prototype]] 存在性能问题
从技术上讲,我们可以在任何时候修改对象的 [[Prototype]],但通常只在创建对象时设置一次。JavaScript 引擎对原型链的修改进行了高度优化,因此“即时”更改原型(如使用 Object.setPrototypeOf 或 obj.__proto__=)是一个非常缓慢的操作。它会破坏对象属性访问的内部优化,导致性能下降。因此,除非必要,否则应避免修改已存在的对象的 [[Prototype]]。