JS原理 - 构造函数、原型与继承详解
构造函数、原型的基础知识和继承的实现方式
原型
每一个对象(null 除外)都有其原型对象,对象以原型对象为模板,从原型继承方法和属性。
原型链
原型对象也可能拥有原型,并从中继承方法和属性,一层一层以此类推,这种关系就被称为原型链(下图蓝色线条)。
当查找某个属性或者方法时,会通过__proto__
属性,一级一级向上查找,直到原型链上的所有__proto__
都被查找完了,才返回undefined
。
小贴士:因为__proto__
不是规范中规定的,是浏览器实现的,所以尽可能不通过__proto__
去获取原型,可以通过Object.getPrototypeOf()
去获取,
属性
__proto__
、 constructor
属性是对象所独有的,而prototype
属性是函数独有的,函数作为一种特殊的对象,所以也会拥有__proto__
、 constructor
属性。
这里需要注意一点的是,修改原型属性并不会影响构造函数的属性,实例会通过__proto__
逐级向上查找原型,而构造函数的__proto__
依次指向Function.prototype
、Object.prototype
、null
,没有经过构造函数的原型。举个例子:
function foo(){};
foo.prototype.friend = 'Nick';
console.log(foo); // ƒ foo(){}
console.log(foo.friend); // undefined
console.log(foo.prototype.friend); // Nick
原型属性
每个函数都有原型属性(prototype),下面以函数 foo 来举例:
function foo(){};
console.log(foo.prototype);
// output
{
constructor: ƒ foo(), // 指向构造函数
__proto__: { // Function的原型对象
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(), // A.hasOwnProperty(B) 判断A是否含有自有属性B
isPrototypeOf: ƒ isPrototypeOf(), // A.isPrototypeOf(B) 判断A是否存在于B的原型链上
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
当添加属性到函数的原型上时:
function foo(){};
foo.prototype.friend = 'Nike';
console.log(foo.prototype);
// output
{
friend: 'Nike',
constructor: ƒ foo(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
自有属性
在函数内部定义的属性叫做自有属性:
function f() {
this.a = 1;
this.b = 2; // 这里的 a 和 b 就是自有属性
}
自有属性和原型属性有什么区别?
在构建函数中,自有属性优先级会高于原型的属性,即存在相同名称时,自有属性会覆盖原型属性;
在实例中修改引用类型的自有属性 A 时不会导致其他实例的属性 A 发生变动,因为这些属性 this 指向了 new 运算符创建出的不同的对象;
但是引用类型的原型属性 B 在修改时,继承于同一父类的实例内的属性 B 都会发生更改;
所以一般都在构造函数内定义属性,在 prototype 内定义方法。
构造函数
构造函数就是 new 关键字创建实例时调用的函数,是生成实例的模板,下述例子中 foo 就是构造函数:
function foo(friendName){
// 自有属性
this.friend = friendName;
}
// 原型属性
foo.prototype.color = "red";
foo.prototype.eat = function(){
console.log('eatting')
}
new 运算符做了什么?
构造函数会经历以下 4 个步骤:
- 创建一个对象:let newObj = {};
- 将构造函数的原型赋值给新对象:Object.setPrototypeOf( newObject , foo.prototype);
- 更改构造函数 this 指向新对象,然后执行构造函数的代码:foo.call( newObj );
- 返回新对象;
注意点
new 运算符会继承构造函数的自有属性和原型属性,但是不会继承静态属性(通过原型链查找属性值),举个例子:
function foo(friendName){
// 自有属性
this.friend = friendName;
}
foo.extra = 'hello';
foo.prototype.sex = 'female';
const Foo = new foo("Nick");
console.log(Foo.friend); // output: Nick
console.log(Foo.extra); // output: undefined
console.log(Foo.sex); // output: female
继承
原型链继承
原型链继承的方法就是重写原型,将子构造函数的 prototype 指向父构造函数的实例:
function Parent () {
this.friend = 'Nick';
this.getFriend = function () {
console.log(this.friend);
}
}
function Child () {}
Child.prototype = new Parent(); // 重写原型,会影响后续创建的实例
const child1 = new Child();
console.log(child1.friend); // 'Nick'
这里将 prototype 指向new Parent()
创建出的实例,而非 Parent.prototype ,是为了继承 Parent 的自有属性 friend 和 getFriend。
缺点:
父类的引用类型的自有属性会被所有实例共享
这是因为在修改 child1 的 friend 属性时,因为其构造函数 Child 内没有 friend 属性,所以 child1 也没有 friend 属性,于是沿着原型链(__proto__)往上寻找 friend 属性,__proto__ 指向的是 Parent 创造出的实例对象,并在这里找到了自有属性 friend,由于 friend 属于引用类型,所以一旦修改,指向该内存地址的所有涉及到的 friend 的值都会被修改。
function Parent () { this.friend = ['kevin']; this.getFriend = function () { console.log(this.friend); } } function Child () {} Child.prototype = new Parent(); const child1 = new Child(); child1.friend.push('mike'); console.log(child1.friend); // ['kevin','mike'] const child2 = new Child(); console.log(child2.friend); // ['kevin','mike']
这里还有一个问题,引用类型会被所有实例共享,那么基础类型是否会被共享?
答案是不能被共享,因为访问原型中的基本类型时,访问到的其实是他的映射副本,对于基本类型的值的修改只有在当前实例中生效,举个例子:
function Parent () { this.friend = 'kevin'; this.getFriend = function () { console.log(this.friend); } } function Child () {} Child.prototype = new Parent(); const child1 = new Child(); child1.friend = 'molly'; console.log(child1.friend); // molly const child2 = new Child(); console.log(child2.friend); // kevin
创建实例的时候无法向父类型(Parent)传参
无法实现多继承
借用构造函数继承
借用构造函数继承就是在子类的构造函数内部调用父类的构造函数,这样一来父类型构造函数的内容就赋值给了子类型的构造函数。
这种继承方式,避免了引用类型的自有属性被所有实例共享,且可以通过 Child 向 Parent 传参。
function Parent () {
this.friend = ['kevin', 'mike']; // 要实现非全局共享,这里的引用类型对象需要是新创建的
}
function Child () {
Parent.call(this);
}
const child1 = new Child();
child1.friend.push('yayu');
console.log(child1.friend); // ["kevin", "mike", "yayu"]
const child2 = new Child();
console.log(child2.friend); // ["kevin", "mike"]
缺点:
- 部分继承,只能继承父类的自有属性,不能继承父类原型的属性和方法
组合式继承
将原型链继承和借用构造函数继承相结合,在子类里面调用父类的构造函数,继承其自有属性,避免自有属性的全局共享;然后通过改写原型对象,让子类和父类的原型对象保持一致,这样子类就能获取到原型链上的属性和方法。
function Parent (number) {
this.friend = ['kevin', 'mike'];
this.sayNumber = function(){
console.log(number)
}
}
function Child (number) {
Parent.call(this,number); // 先借用构造函数继承,复制父类型构造函数的内容
}
Child.prototype = Parent.prototype; // 后重写原型
Child.prototype.constructor = Child; // 修复构造函数指向
const child1 = new Child(2);
child1.friend.push('yayu');
console.log(child1.friend); // ["kevin", "mike", "yayu"]
child1.sayNumber(); // 2
const child2 = new Child(3);
console.log(child2.friend); // ["kevin", "mike"]
寄生式组合继承
组合式继承直接修改了原型的指向Child.prototype = Parent.prototype;
,此时如果在子类的原型上追加方法,会影响到父类的原型。而寄生式组合继承需要用到Object.create()
,作用是:创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__,也就是原型对象,需要注意的是,这是浅拷贝。
function Parent (number) {
this.friend = ['kevin', 'mike'];
this.sayNumber = function(){
console.log(number)
}
}
function Child (number) {
Parent.call(this,number);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.test = '123';
console.log( Parent.prototype ); // 找不到test
Class 继承
ES6 加入的 class 属性提供了简单的继承方式:extends
class Animal {
constructor(name) {
super();
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // 调用父类的构造函数
}
speak() {
console.log(`${this.name} barks.`);
}
oldSpeak() {
super.speak(); // 调用父类的方法
}
}
let d = new Dog('Mitzie')
d.speak() // Mitzie barks.
d.oldSpeak() // Mitzie makes a noise.
ES5 和 ES6 继承方式的区别在于:ES5 是先创造子类的实例对象,然后再将父类的方法和属性添加到这个实例对象上;ES6 则是先创建父类的实例对象(在 constructor 内需要先调用 super()
),然后再使用子类的构造函数修改实例对象。
JS原理 - 构造函数、原型与继承详解