前言
关于原型链继承一直是JS中比较重要的知识点,最近也浏览了一些资料与总结,现在整理一下,做个学习笔记
一、基本概念
new
运算符
我们经常使用new运算符和自定义构造函数来实例化对象:
function Person(name, age, movement){
this.name = name;
this.age = age;
this.movement = movement;
}
var person1 = new Person('王五', 20, function(){
console.log('studying~');
});
实际上new运算符在调用构造函数时,函数内部的代码发生以下变化:
function Person(name, age, movement){
var this = new Object();
this.name = name;
this.age = age;
this.movement = movement;
return this;
}
此时,构造函数内的this
就指向实例化的对象,保证各个实例对象的独立性
实例属性和实例方法
- 概括:都是绑定在使用构造函数所创建的对象上,并且通过该实例对象来访问
- 上述的
name
、age
、movement
就是实例属性和实例方法
- 上述的
静态属性和静态方法
- 首先要明确,在JS里,函数也是对象,所以函数也可以动态地添加属性和方法
- 静态属性和方法就是绑定在函数自身上的属性和方法(非实例对象上的)
function Person(name, age, movement){
this.name = name;
this.age = age;
this.movement = movement;
// Person.personCount就是一个静态属性,用来记录创建了几次实例对象
if(!Person.personCount){
Person.personCount = 0;
}
Person.personCount++;
}
// Person.printPersonCount就是静态方法,可以直接调用
Person.printPersonCount = function(){
console.log('共创建了' + Person.personCount + '个实例对象');
}
var person1 = new Person('王五', 20, function(){
console.log('studying~');
});
Person.printPersonCount(); // 1
内置对象类型的判断
当我们想知道一个引用类型对象的内置对象类型时,使用typeof
打印结果是不准确的,因为很可能返回的都是object
,此时我们可以使用以下两种方法:
(对象).constructor.name
Object.prototype.toString.call(对象)
var obj = {name: '王五'};
console.log(typeof obj); // object
console.log(obj.toString()); // [object Object]
console.log(obj.constructor.name); // Object
console.log(Object.prototype.toString.call(obj)); // [object Object]
var arr = [1, 2, 3];
console.log(typeof arr); // object
console.log(arr.toString()); // 1, 2, 3
console.log(arr.constructor.name); // Array
console.log(Object.prototype.toString.call(arr)); // [object Array]
var date = new Date();
console.log(typeof date); // object
console.log(date.toString()); // Sun Nov 17 2019 11:48:23 GMT+0800 (China Standard Time)
console.log(date.constructor.name); // Date
console.log(Object.prototype.toString.call(date)); // [object Date]
自定义对象类型的判断
上面提到在判断内置对象类型时,可以使用:Object.prototype.toString.call(对象)
、(对象).constructor.name
方法,可是在判断自定义构造函数所创建出的对象时,这两种方法都有效吗?
function Dog(name, age){
this.name = name;
this.age = age;
}
function Cat(name, age){
this.name = name;
this.age = age;
}
var dog = new Dog('小狗', 1);
var cat = new Cat('小猫', 2);
console.log(dog.constructor.name); // Dog
console.log(cat.constructor.name); // Cat
console.log(Object.prototype.toString.call(dog)); // [object Object]
console.log(Object.prototype.toString.call(cat)); // [object Object]
可以发现此时Object.prototype.toString.call(对象)
方法“失效”了。原因在于,别忘了上面提到的new
的作用
function Dog(name, age){
var this = new Object();
this.name = name;
this.age = age;
return this;
}
所以Object.prototype.toString.call(对象)
方法是查找实际创建出对象的对象类型
instanceof
操作符
用来判断实例与原型的继承关系,只要是原型链上的构造函数,都会返回true
isPrototypeOf
操作符
用来判断某个对象是不是某个实例的原型对象
访问函数原型对象的方式
函数名.prototype
- 通过对象中的
__proto__
属性访问- 注意:
__proto__
是一个非标准属性,它只是某些浏览器为方便开发和调试而提供的属性,所以应该避免在正式代码中出现
- 注意:
hasOwnProperty
和in
属性操作
两者都是用来判断某一对象上是否拥有某个属性,但不一样的在于:
hasOwnProperty
: 只在对象自身上查找是否有某个属性in
:当对象自身不具有该属性时,会继续到原型链上的原型对象中查找
二、原型链继承
画出Date
的原型链
单纯的原型链继承
function Person(){
this.hobbies = ['阅读','看电影'];
}
Person.prototype.run = function(){
console.log("跑步");
}
function Student(){
this.school = 'xx小学';
}
Student.prototype = Person.prototype; // 修改原型对象的指向
var stu = new Student():
stu.run(); // 跑步
可以看到通过修改原型对象的指向,确实可以调用Person原型对象上的run方法,但是只能访问到Person原型对象上的属性和方法,而Person构造函数中的实例属性和方法访问不到
借助构造函数(组合继承)
function Person(){
this.hobbies = ['阅读','看电影'];
}
Person.prototype.run = function(){
console.log("跑步");
}
function Student(){
this.school = 'xx小学';
}
// 1. 构造父类实例
var person = new Person();
// 2. 修改Student原型对象为父类实例
Student.prototype = person;
var stu = new Student():
stu.run(); // 跑步
console.log(stu.hobbies[0]); // 阅读
这下,Student构造函数创建的实例就可以访问到Person类的原型属性、方法和实例属性、方法,但是新的问题出现了:打印stu.constructor.name
,结果为Person
,所以还要解决这个问题:
// 1. 构造父类实例
var person = new Person();
// 2. 修改Student原型对象为父类实例
Student.prototype = person;
// 3. 修复constructor指向
Student.prototype.constructor = Student;
var stu = new Student():
stu.run(); // 跑步
console.log(stu.hobbies[0]); // 阅读
console.log(stu.constructor.name); // Student
借助构造函数继承的问题
上面提到借用构造函数继承,但是有一个问题:继承过来的实例属性如果是引用类型,会被多个子类的实例共享
function Person(){
this.hobbies = ['阅读','看电影'];
}
Person.prototype.run = function(){
console.log("跑步");
}
function Student(){
this.school = 'xx小学';
}
// 1. 构造父类实例
var person = new Person();
// 2. 修改Student原型对象为父类实例
Student.prototype = person;
// 3. 修复constructor指向
Student.prototype.constructor = Student;
var stu1 = new Student():
stu1.hobbies.push('睡觉');
var stu2 = new Student();
console.log(stu2.hobbies); // ['阅读','看电影', '综艺']
这是因为在子类的实例对象上找不到hobbies数组,就会到原型链上查找,所以子类的实例对象对hobbies数组的修改实际上操作的是同一块内存空间,可以使用call
、apply
来替代
call
、apply
继承
可以使用call
、apply
继承父类的实例属性和方法
function Person(){
this.name = '王五';
this.age = 20;
this.hobbies = ['阅读','看电影'];
}
function Student(){
Person.call(this);
// Person.apply(this);
// 使用call apply相当于把父类的实例属性和方法复制到了子类,
// 保证了其独一性,不会发生父类引用类型的实例属性被多个子类实例共享的情况
}
寄生组合式继承
以上的继承模式可以让子类同时访问到父类的实例属性、方法和原型对象上的属性、方法,但还有一个弊端:必须要调用两次父类构造函数,导致父类属性重复
所以最终可以使用寄生式组合继承
的方式完美继承
function Person(name, age){
this.name = name;
this.age = age;
this.hobbies = ['阅读','看电影'];
}
Person.prototype.run = function(){
console.log("跑步");
}
function Student(name, age){
// 借调 访问父类构造函数中的实例属性和方法
Person.call(this, name, age);
this.school = 'xx小学';
}
// 寄生式继承 访问父类原型对象上的属性和方法
function Temp(){}
Temp.prototype = Person.prototype;
var studentProto = new Temp();
Student.prototype = studentProto;
studentProto.constructor = Student;