js 原型、原型链、继承
在JavaScript
中,原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript的对象中都包含了一个[[Prototype]]
内部属性,这个属性所对应的就是该对象的原型。[[Prototype]]
作为对象的内部属性,是不能被直接访问的。所以为了方便查看一个对象的原型,Firefox和Chrome中提供了__proto__
这个非标准(不是所有浏览器都支持)的访问器(ECMA引入了标准对象原型访问器Object.getPrototype(object)
)。在JavaScript
的原型对象中,还包含一个constructor
属性,这个属性对应创建所有指向该原型的实例的构造函数
原型对象,对象原型
构造函数,实例,原型对象三者的关系可以使用一张图来理解
原型对象prototype
每一个构造函数都有一个属性prototype
,我们也称之为原型对象,原型对象另外开辟一个内存空间,用来存放方法,作用是为了共享方法,从而达到节省内存
function Person(uname, age) {
this.uname = uname;
this.age = age;
};
Person.prototype.eat = function () {
console.log('吃饭');
};
let jerry = new Person('jerry', 18);
jerry.eat();
// 实例对象有一个__proto__属性,可以指向该实例对象的构造函数的原型对象
console.log(jerry.__proto__);
console.log(Person.prototype);
console.log(jerry.__proto__ === Person.prototype); // true
原型对象属性constructor
原型对象上有个属性constructor
,用来指向该原型对象的构造函数
function Person(uname, age) {
this.uname = uname;
this.age = age;
};
// 这里指向的就是Person构造函数
console.log(Person.prototype.constructor === Person); // true
可以通过Person.prototype.run = function(){}
的方式添加方法
以这种方式创建方法的话,原型对象上其中有个自带的属性constructor
,用来指向原型对象的构造函数
function Person(uname, age) {
this.uname = uname;
this.age = age;
};
Person.prototype.sing = function () {
console.log('sing');
}
Person.prototype.jump = function () {
console.log('jump');
}
console.log(Person.prototype);
另外一种创建方法的方式,这种方式创建会覆盖原有的原型对象,且原型对象上的constructor
属性也会没有,所以必须重新将constructor
指向构造函数
function Person(uname, age) {
this.uname = uname;
this.age = age;
};
Person.prototype = {
rap: function () {
console.log('rap');
},
basketball: function () {
console.log('basketball');
},
// 重新指向构造函数
constructor: Person,
}
console.log(Person.prototype);
对象原型__proto__
实例对象有一个属性,可以指向该实例对象的构造函数的原型对象
function Person(uname, age) {
this.uname = uname;
this.age = age;
};
let jerry = new Person('jerry', 18);
// 实例对象有一个属性,可以指向该实例对象的构造函数的原型对象
console.log(jerry.__proto__);
console.log(Person.prototype);
console.log(jerry.__proto__ === Person.prototype); // true
原型链
当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
如果没有就查找它的原型(也就是__proto__
指向的prototype
原型对象)。
如果还没有就查找原型对象的原型(Object
的原型对象)。
依此类推一直找到Object
为止(null
)。__proto__
对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
function Person(name, age) {
this.name = name;
this.age = age;
}
let jerry = new Person('jerry', 18);
// Person.prototype指向Person构造函数的原型对象
// jerry.__proto__也指向Person构造函数的原型对象
// 原型对象也是对象,也有__proto__方法,原型对象的原型对象是Object
console.log(Person.prototype.__proto__);
// Object原型对象的构造函数是Object()构造函数
console.log(Person.prototype.__proto__.constructor);
// 在往上找Object原型对象的原型对象,是null,这个就是原型链的顶端,并不是无限下去的
console.log(Person.prototype.__proto__.__proto__);
对象的原型链
构造函数的原型链
继承
原型链继承
// 原型链继承,定义一个父类
function Father(name) {
this.fatherName = [name]
}
// 父类定义一个方法
Father.prototype.printFatherName = function () {
console.log(this.fatherName);
}
// 定义一个子类
function Son(name) {
this.sonName = name
}
// 把子类的原型对象指向父类的实例
Son.prototype = new Father('Father')
// 由于覆盖了原有的prototype,需要将它上面的constructor指向子类,否则原型链就会混乱
Son.prototype.constructor = Son
Son.prototype.printSonName = function () {
console.log(this.sonName);
}
// 实例化子类,此时子类就继承了父类的printFatherName方法(原型链方法会向上查找)
let s = new Son('Son')
s.printFatherName() // ["Father"]
s.printSonName() // Son
console.log(s.fatherName); // ["Father"]
console.log(s.sonName); // Son
// 原型链继承的缺陷,对于引用数据类型,如果Son有多个实例,其中一个实例对继承的父类属性做了修改,那么所有实例都会随之修改
let s1 = new Son('Son1')
s1.printFatherName() // ["Father"]
s1.printSonName() // Son1
console.log(s1.fatherName); // ["Father"]
console.log(s1.sonName); // Son1
s.fatherName.push('newName') // s 对继承的属性做了修改,此时 s1 也会随之修改
console.log(s.fatherName); // ["Father", "newName"]
console.log(s1.fatherName); // ["Father", "newName"]
构造函数继承
使用call
改变了this
指向的继承方式,称为构造函数继承
function Father(name) {
this.fatherName = [name]
}
Father.prototype.printFatherName = function () {
console.log(this.fatherName);
}
function Son(name) {
// 使用call改变了this指向,
// 调用Father函数就相当于是把函数执行了一遍,在子类里面创建了一个和父类相同的属性,但是this是指向的子类实例
Father.call(this, name)
}
let s = new Son('Son')
console.log(s.fatherName);
let s1 = new Son('Son1')
console.log(s1.fatherName);
// 解决了多个子类修改父类属性造成同步更改的问题
s.fatherName.push('newName')
console.log(s.fatherName); // ["Son", "newName"]
console.log(s1.fatherName); // ["Son1"]
// 但是又有个问题就是子类无法继承父类的方法
s.printFatherName() // 报错 Uncaught TypeError: s.printFatherName is not a function
组合继承
把原型链继承和构造函数继承结合使用,就是组合继承
function Father(name) {
this.name = name
this.arr = [1, 2, 3]
}
Father.prototype.printName = function () {
console.log(this.name);
}
// 构造函数继承父类的方法和属性
function Son(name, age) {
Father.call(this, name)
this.age = age
}
// 原型链继承父类原型上的方式和属性
Son.prototype = new Father()
Son.prototype.constructor = Son
Son.prototype.printSonAge = function () {
console.log(this.age);
}
let s = new Son('son', 18)
s.printSonAge()
s.printName()
console.log(s.arr);
s.arr.push(4)
// 可以调用父类原型上面的方法,实例的属性修改也不会影响其他实例的属性
let s1 = new Son('son1', 20)
s1.printSonAge()
s1.printName()
console.log(s.arr); // [1, 2, 3, 4]
console.log(s1.arr); // [1, 2, 3 ]
// 缺点:会调用两次父类的构造函数,第一次是在 new Faher() 的时候, 第二次是在new Son()的时候(new Son() 里面会执行 Father.call)
// 因此实例的原型对象上会有两份相同的属性,解决这个问题可以用寄生组合继承
原型式继承
let Father = {
name: 'Father',
friends: ['A', 'B', 'C'],
printName: function () { console.log(this.name); }
}
// 原型式继承原理是,创建一个中间实例,中间实例的原型指向父类(这个父类此时是一个对象的实例)
// 这个函数是Object.create函数的模拟实现
function createObj(obj) {
function F() { }
F.prototype = obj
return new F()
}
let Son1 = createObj(Father)
let Son2 = createObj(Father)
// 使用下面Object.create()函数是一样的
// let Son1 = Object.create(Father)
// let Son2 = Object.create(Father)
// 这里修改name值,并不是修改了原型上的name,而是给自己身上加了一个name属性
Son1.name = 'Son1'
Son2.name = 'Son2'
Son1.printName()
Son2.printName()
// 缺点:原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
Son1.friends.push('D')
console.log(Son1.friends); // ["A", "B", "C", "D"]
console.log(Son2.friends); // ["A", "B", "C", "D"]
寄生式继承
let Father = {
name: 'Father',
friends: ['A', 'B', 'C'],
printName: function () { console.log(this.name); }
}
// 寄生式继承主要用来增强对象
function createObj(obj) {
var clone = Object.create(obj)
clone.printAge = function () { console.log(this.age); }
return clone
}
let son1 = createObj(Father)
son1.name = 'jerry'
son1.age = 18
son1.printName()
son1.printAge()
let son2 = createObj(Father)
son2.age = 20
son2.printName()
son2.printAge()
寄生组合继承
寄生组合继承应该算是es5中最完美的继承方式
function Father(name) {
this.name = name
this.friends = ['A', 'B']
}
Father.prototype.printName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
// 创建一个中间函数
let tempFunc = function () { }
// 中间函数的prototype指向Father的原型对象
tempFunc.prototype = Father.prototype
// 子类的prototype指向中间函数的实例
Son.prototype = new tempFunc()
// 修正constructor指针
Son.prototype.constructor = Son
// 子类增加一个方法,对其他继承Father的子类没有影响
Son.prototype.printAge = function () { console.log(this.age); }
let son1 = new Son('jerry', 18)
son1.printName() // jerry
son1.printAge() // 18
let son2 = new Son('tom', 20)
son2.printName() // tom
son2.printAge() // 20
son1.friends.push('D')
console.log(son1.friends); // ["A", "B", "D"]
console.log(son2.friends); // ["A", "B"]
// 有人要问,为什么不能直接Son.prototype = Father.prototype,
// 这是因为如果多个子类,都指向同一个父类的prototype,子类的独立性就没有了,
// 如果其中一个子类对原型上面的东西做了修改,其他所有子类都会发生变化
// 其他子类
function Child(name, age) {
Father.call(this, name)
this.age = age
}
let F = function () { }
F.prototype = Father.prototype
Child.prototype = new F()
Child.prototype.constructor = Child
// 子类的原型上添加属性或方法,不会影响其他子类
Child.prototype.printAge = function () { console.log('child age', this.age); }
let c1 = new Child('child1', 22)
c1.printName() // child1
c1.printAge() // child age 22
ES6 class 继承
先看一个简单的例子
class Father {
constructor(name, age) {
this.name = name;
this.age = age;
}
run() {
console.log('I\'m running, by father');
}
}
class Son extends Father {
constructor(name, age, birth) {
super(name, age);
this.birth = birth;
};
run() {
super.run();
}
fly() {
console.log('I\'m flying');
}
}
let son = new Son('jerry', 18, '2000');
console.log(son.name); // jerry
console.log(son.age); // 18
son.run(); // I'm running, by father
son.fly(); // I'm flying
一个稍微复杂的例子
class fatherCalculator {
constructor(num1, num2) {
this.num1 = num1;
this.num2 = num2;
};
sum() {
return this.num1 + this.num2;
}
multiply() {
return this.num1 * this.num2;
}
log() {
return [this.num1, this.num2];
}
}
let cal = new fatherCalculator(1, 2);
console.log(cal.sum());; // 3
// super关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数
// super关键字指向当前对象的原型对象, sonCalculator.prototype 指向的是 fatherCalculator
class sonCalculator extends fatherCalculator {
constructor(num1, num2) {
super(num1, num2);
};
sum() {
return super.sum()
};
reduce() {
return this.num1 - this.num2;
}
}
let cal2 = new sonCalculator(3, 4);
console.log(cal2);
console.log(cal2.sum()); // 7
console.log(cal2.reduce()); // -1
// 如果子类中没有方法,会向父类中去找,就近原则
console.log(cal2.multiply()); // 12
// 当子类没有constructor的时候可以随意用父类的,但是如果子类也含有的话,constructor会返回实例,this的指向不同,不可以再直接使用父类的东西
class son extends fatherCalculator { };
let cal3 = new son(10, 10);
console.log(cal3.sum()); // 20
class son1 extends fatherCalculator {
constructor(a, b, c) { // 前两个参数是父类构造函数中的参数,无法在子类中拿到,但是可以调用父类的方法来获取到
super(a, b) // 子类constructor中必须先使用super,否则报错继承 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
this.c = c
}
log() {
// 在子类中无法直接拿到父类中的属性,但是可以在父类中暴露api,子类中进行获取
console.log(super.log()); // [1, 2]
// 这里无法拿到a, b参数,只能拿到自己的属性c
console.log(this.a, this.b, this.c); // undefined undefined 33
return [super.sum(), this.c]
}
}
let cal4 = new son1(1, 2, 33)
console.log(cal4.log()); // [3, 33]
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com