你有没有过这般的疑虑:清清楚楚仅仅是声明了一个空的数组,为何它自身就附带了push、pop这类方法?这些方法既并非是你亲自撰写的,且在数组对象自身那里也寻觅不到,它们到底是从哪里“冒”出来的?这背后实则是JavaScript的核心机制——原型链在发挥作用。弄明白它,就切实把握住了JavaScript面向对象编程的关键所在了。
const arr = [1, 2, 3];
arr.push(4); // 哪里来的push?
于JavaScript的范畴之内,每一个对象皆能够拥有一个“备用”对象,此“备用”对象即为原型。当你尝试去访问一个对象的属性或者方法之际,若该对象自身并不具备这个属性,JavaScript引擎不会径直报错,而是会悄然地前往它的原型对象那里去寻觅。
这个机制类似家族之中的“遗产继承”,要是你自家没有某样物品,你便会前往你的直系长辈家中寻觅,原型恰恰充当了这般“长辈”的角色,它给对象给予了一份能够共享的属性以及方法清单,极大程度地节省了内存并且达成了代码复用。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
const zhangsan = new Person('张三');
zhangsan.sayHello(); // 你好,我是张三
每个函数于创建之际,皆会自动具备一个prototype属性,此属性指向一个普通对象,该对象被称作此函数的“原型对象”,当此函数用作构造函数时,即在借由new关键字创建实例时,新生成的实例内部便会关联至这个prototype对象之上。
console.log(zhangsan.__proto__ === Person.prototype); // true
在同一时间,JavaScript里边几乎每一个对象(对象里除了null之外),都有着一个具备的内部属性,在相关浏览器环境之内常常借助__proto__去探访。这个属性如同一条脐带,彼此连接着现今对象以及它的构造函数的prototype。恰恰是经由它,实例对象才能够去访得原型里所定义的全部属性以及众多方法。
console.log(Person.__proto__ === Function.prototype); // true
当你进行obj.property调用之时,一场顺着原型链所开展的搜索便已然起始,引擎率先会核查obj自身是不是存在名为property的属性,要是存在,便径直返回,要是不存在,引擎会本着对obj.__proto__这条链向上遵循的原则,前往它的原型对象那儿持久探寻。
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name}在吃东西`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log('汪汪汪');
};
const wangcai = new Dog('旺财', '土狗');
wangcai.bark(); // 汪汪汪 (自己原型上的)
wangcai.eat(); // 旺财在吃东西 (从Animal原型继承来的)
wangcai.toString(); // [object Object] (从Object原型来的)
此过程会一层一层地朝着上方追溯,直至寻得该属性,或者到达原型链的顶端——也就是 Object.prototype。要是在 Object.prototype 上也未找到,而且 Object.prototype.__proto__ 为 null,那么引擎便会停止搜索,最终返回 undefined。之所以所有普通对象都能够调用toString方法,原因便在于此。
console.log(Object.prototype.__proto__); // null
在原型链里处于顶端位置的是Object.prototype, 在具备自身特性的情况下它的原型是null,这表明着对于查找而言这是终点,在Object.prototype之上被定义了一些全部对象都拥有的基础方法, 像是hasOwnProperty、valueOf以及toString。正因为是这样的缘故,不管是数组,还是函数,亦或是正则表达式,它们全部都能够直接去调用这些通用的方法。
原型之上的属性跟方法是共享着的,这表明要是你更改了特定构造函数的prototype,那么经由该构造函数创建出来的所有实例都会即刻“继承”到此项修改,这个特性于需要为全部实例增添统一功能之际极为有用,然而在多人协作的项目里,随意去修改内置对象(像Array.prototype)或许会引发难以预测的问题。
function Person(name) {
this.name = name;
}
Person.prototype.age = 18;
const p = new Person('张三');
console.log(p.hasOwnProperty('name')); // true,自己的
console.log(p.hasOwnProperty('age')); // false,继承的
console.log('age' in p); // true,不管自己的还是继承的,只要能访问到就返回true
在ES6的class语法出现以前的时候,开发者主要是通过操作原型链的方式来达成继承的目的。经典的达成方式是“组合寄生继承”。首先而言,要让子类的prototype对象指向一个不是别的新对象而是由父类原型创建出来的新对象,然后,要去修正子类prototype的那个constructor 属性,最后,要在子类构造函数的内部借助父类的构造函数去初始化实例属性。
这种模式,清晰地将JavaScript基于原型的继承本质给展示了出来。代码写起来,虽说会略显烦琐,不过它完整地针对传统面向对象语言里面的继承关系进行了模拟。如今,我们能够使用更简约的class与extends关键字,然而其底层依旧是这一套原型机制,仅仅是提供了一种更为清晰、更便于使用的语法糖。
function Person() {}
const p1 = new Person();
const p2 = new Person();
Person.prototype.say = function() {
console.log('hello');
};
p1.say(); // hello
p2.say(); // hello,两个实例都有了
Person.prototype.say = function() {
console.log('world');
};
p1.say(); // world,瞬间都变了
在日常展开项目的开发进程里头,存在着好些关于原型链的最佳实践的要点是值得予以留意,加以关注的。其一,要规避掉直接去运用__proto__属性,尽管它在很大范围之内是被予以广泛支持的,然而它并不是属于标准规范层面的内容。其二,建议采用Object.getPrototypeOf以及Object.setPrototypeOf这两种方式来获取或者设置一个对象的原型,通过这样的做法会显得更为安全,并且在性能方面也会更优一些。
当要创建那种需要特定原型的新对象之际,Object.create 乃是最为简单直接的办法。它能够让你直接去指定一个新对象的原型,并不需要去定义构造函数。最终,在现代的项目当中,应当优先运用 class 语法去处理对象之间的继承关系,它会使得代码的意图更加明确,可以有效地减少因为手动操作原型而引发的错误。
// 1. 定义父类构造函数
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name}吃东西`);
};
// 2. 定义子类构造函数
function Dog(name, breed) {
Animal.call(this, name); // 继承属性
this.breed = breed;
}
// 3. 继承方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 4. 添加子类自己的方法
Dog.prototype.bark = function() {
console.log('汪汪汪');
};
此刻,你能够尝试思索一番:于你近期所编写的代码里头,是否存在某个位置,要是能够借助原型链的共享特性予以重构,就能让代码变得更为简洁高效?踊跃在评论区把你的经验加以分享,也千万别忘了赞同并转发,以使更多的朋友一道梳理明晰这个关键的JavaScript概念。
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name}吃东西`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
console.log('汪汪汪');
}
} 相关标签: # 原型链 # JavaScript # 面向对象 # 继承 # 属性