面向对象原型继承(ES5)
普通对象原型(隐式原型)
JavaScript中的每个对象都有一个特殊的内置属性
[[prototype]]
,它实际上是一个对象,指向的是另外一个对象
[[prototype]]
的作用- 当我们通过引用对象的 key获取一个value时候,实际上会触发
get
的对象操作 - 这个操作 首先会查找对象本身有没有这个key,如果有的话直接使用
- 没有这个 key,那么会访问对象内置属性
[[prototype]]
指向的对象上,有没有这个属性
- 当我们通过引用对象的 key获取一个value时候,实际上会触发
let obj = {
a: 100,
b: 200,
};
//__proto__是浏览器自动加的
console.log(obj.__proto__);
//这是标准获取方法
console.log(Object.getPrototypeOf(obj));
函数的原型(显式原型)
所有的函数都有一个prototype属性 注意不是
__proto__
- 我们知道,一个函数可以当作构造函数来使用,通过new,创建新的对象
function Foo(){
}
/*
通过new方法创建的对象,内部会帮我们做以下事情
1.首先在函数内部创建一个空对象
2.将对象的__proto__(隐式原型)指向函数的prototype(显式原型的作用)
3.将空对象赋值给this
4.执行函数体中的代码
5.将这个对象默认返回
*/
let newFoo = new Foo()
//通过步骤2我们可以得知,newFoo对象的隐式原型就是Foo函数的显式原型
newFoo.__proto__ === Foo.prototype
//无论通过Foo函数创建多少对象,这些对象的隐式原型都是相等的,均指向Foo函数的显式原型
-
通过以上代码,将函数的显式原型赋值给对象的隐式原型就是 显式原型的作用;
-
那么知道了它的作用,接下来可以看一个小案例,加深以下体会(将 方法放在原型上)
-
首先我们知道,我们可以通过构造函数来创建一个又一个的对象,这是为了将多个对象的共同的内容抽象到一起
//创建一个学生构造函数 function Student(stuName,age){ this.stuName = stuName; this.age = age; //每个学生都有这个方法 this.study = function () { console.log(this.stuName + "正在学习"); }; } //这样我们就可以创建多个对象 let stu1 = new Student("zhangsan",18) let stu2 = new Student("zhangsan2",18) let stu3 = new Student("zhangsan3",18) let stu4 = new Student("zhangsan4",18)
-
但是新的问题随之而来,每创建一个对象,都会生成一个study的方法,当生成足够多对象的时候,会占用内存
-
那么通过 显式原型和隐式原型的关系我们可以知道,将共有的方法提取到 显式原型上即可
function Student(stuName, age) { this.stuName = stuName; this.age = age; } Student.prototype.study = function () { //这里的this指向,在前面的文章解释过,通过隐式绑定,指向的就是调用方法的对象 console.log(this.stuName + "正在学习"); }; let stu1 = new Student("zhangsan", 20); let stu2 = new Student("lisi", 18); console.log(stu1.stuName, stu1.age); console.log(stu2.stuName, stu2.age); stu1.study() stu2.study()
-
通过 将方法提取到构造函数的显式原型上,就可以解决以上的问题,以下是模拟的内存图
-
通过内存模拟图我们可以看出
- 无论创建多少对象,study方法都只会有一份
- 每个对象都有 隐式原型,这个 隐式原型是通过构造函数的 显式原型赋值过去的
- 因此对象在调用study方法的时候:首先在自己身上找,发现没有就会顺着原型链找到所指向的显式原型
-
因此当多个对象拥有共同的值时,我们可以放到构造函数的显式原型中
- 由构造函数创建的所有对象,都会共享这些属性
-
显式原型中的属性
显式原型中有一个属性construtor
-
我们打印显式原型,会发现有一个construtor的属性,同时打印这个属性发现是函数本身
function Foo(){ } console.log(Foo.prototype)//construtor console.log(Foo.prototype.construtor)//Foo
-
试着画出以下代码的内存图
function Student(stuName, age) { this.stuName = stuName; this.age = age; } Student.prototype.study = function () { console.log(this.stuName); }; let stu1 = new Student("zhangsan", 20); let stu2 = new Student("lisi", 18); stu1.address = "河北"; stu1.num = "18213"; Student.prototype.classRoom = "ruanjian"; stu1.classRoom = "jisuanji"; console.log(stu2.classRoom); stu1.study();
- 结合内存图,再看代码的运行情况,就可以明白以上的几点结论
- 构造函数有自己的 显式原型对象
- 构造函数创建对象时候,将对象的 隐式原型,指向自己的显式原型对象
- 对象在调用方法,访问对象的时候,会优先在自己身上查找,没有的话再通过 隐式原型去查找
- 结合内存图,再看代码的运行情况,就可以明白以上的几点结论
重写函数显式原型对象
-
当我们要再函数显式原型上 添加大量的属性以及方法的时候,可以考虑重写显式原型
function Student(stuName, age) { this.stuName = stuName; this.age = age; } Student.prototype.study = function () { console.log(this.stuName); }; Student.prototype.num1 = 123; Student.prototype.num2 = 456; //可以这样重写显式原型对象 Student.prototype = { study:function () { console.log(this.stuName); }, num1:123, num2:456 }
-
但是重写完只会,我们会发现 重写的显式原型对象,缺失了constructor属性
Student.prototype = { study:function () { console.log(this.stuName); }, num1:123, num2:456, //可以直接这样写上该属性 constructor:Student } //但是原本的constructor属性,是不可枚举的,且数据属性描述符需要特殊设置 Object.defineProperty(Student.prototype,constructor,{ value:Student //设置其他的数据属性描述符即可 })
面向对象的特性 – 继承
面向对象的三大特性:封装、继承、多态
- 封装:将多个对象中,相同的属性,写到一个类中的思想就是封装的思想
- 继承:**继承是面向对象中非常重要的特性,**是多态的前提(纯面向对象中)
- 可以帮助我们 **将重复的代码抽取到一个父类中,**子类只需要继承过来使用即可
- 在JS中实现,需要了解继承
- 多态:不同的对象再执行时表现出不同的形态(JS中不明显)
对象的原型链
-
默认形式的原型链
//当我们创建一个对象的时候 //这种创建方式相当于:let obj = new Object() //通过上面的学习,我们可以知道obj.__proto__ === Object.prototy //而Object.prototype作为对象也有自己的隐式原型,它的隐式原型指向null let obj = { name:"zhangcheng" }
-
因此通过以上理论,我们可以对原型对象进行改造
//当我们在obj上面查找message的时候,自身没有,就会顺着去查找它的原型,一层一层的往上找 let obj = { name: "zhangcheng", }; obj.__proto__ = { message: "hello aaa", }; obj.__proto__.__proto__ = { message: "hello bbb", }; obj.__proto__.__proto__.__proto__ = { message: "hello ccc", }; console.log(obj.message);
-
通过把对象的隐式原型改造,让其一层一层的去查找,这样就形成了原型链
-
同时这就是继承的思想,我们可以创建一个父类,创建的子类去 继承父类,子类实例出来的对象,在查找值的时候,首先在自身查找,之后会顺着原型链,去子类查找,子类若没有,就会查找父类,最后会查找到
Object
的原型对象—>null
通过原型链实现继承
既然了解了理论,那么我们就开始使用原型链实现继承(ES5),有可能在使用定义变量用的ES6的语法,但是思想是最重要的
- 现在有Peoson和Student两个类,要实现Student继承Person
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {
console.log("running");
};
function Student(name, age, classroom) {
this.name = name;
this.age = age;
this.classroom = classroom;
}
Student.prototype.running = function () {
console.log("running");
};
Student.prototype.study = function () {
console.log("studying");
};
- 在真正实现继承之前,我们先看以下操作
- 这样虽然可以让stu1对象成功调用running
- 但是在给
Student
类添加方法的时候,实际上是给Person类添加的方法(详见内存图)
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {
console.log("running");
};
//方式一:将Student的显式原型直接指向Peoson的显式原型
//或者将stu1的隐式原型指向Peoson的显式原型
Student.prototype = Person.prototype;
function Student(name, age, classroom) {
this.name = name;
this.age = age;
this.classroom = classroom;
}
//Student.prototype.running = function () {
// console.log("running");
//};
Student.prototype.study = function () {
console.log("studying");
};
let stu1 = new Student("zhangcheng", 18, 2002);
stu1.running();
- 很明显,以上的操作方法,并不是我们想要的
组合借用继承
- 这种方式是ES5最常用的继承方法,但是只是基本实现了继承,依旧存在很多缺点
- 通过第三方,实现父类方法的继承(用父类创建一个对象,让子类的显式原型指向这个对象本身)
- 借用构造函数,实现父类属性的继承(继承最大的用处是提高代码的复用,因此父类存在的属性,子类没有必要再写一份)
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {
console.log("running");
};
//方式一:pass
// Student.prototype = Person.prototype;
//借用第三方,实现方法继承
let p = new Person();
Student.prototype = p;
function Student(name, age, classroom) {
//借用构造函数的方式实现属性继承
Person.call(this, name, age);
this.classroom = classroom;
}
// Student.prototype.running = function () {
// console.log("running");
// };
Student.prototype.study = function () {
console.log("studying");
};
let stu1 = new Student("zhangcheng", 18, 2002);
console.log(stu1.name, stu1.age, stu1.classroom);
stu1.running();
stu1.study();
- 通过组合借用继承的缺点
- 无论在什么情况下 都会调用两次父类的构造函数
- 通过父类创建一个实例,在子类的函数中,通过构造函数继承属性
- 所有的子类实例 事实上会有两份父类属性
- 一份存在实例对象上,另外一份存在父类创建的实例对象上
- 无论在什么情况下 都会调用两次父类的构造函数
寄生组合式继承
是最终的解决方案
原型式继承函数
这种思想是道格拉斯·克罗克福德提出来的
-
为了理解这种思想,我们需要回顾一下上面为了实现方法继承的目的
- 在上面实现 方法继承,我们通过new 一个父类创建一个对象,但是会存在一些弊端
- new 方法实现了以下的功能:
- 1.创建一个空对象;
- 2.将父类的 显式原型赋值给 对象的 隐式原型
-
因此为了 满足以上条件,且 不出现两份父类的元素,就出现了以下代码
//传入的o是父类的显式原型对象 function F(o){ //让该函数的显式原型,指向父类的显式原型 F.prototype = o //返回一个F类的对象,这个对象的隐式原型--->F类的显式原型--->o的显式原型 return new F() } function Person(){ } function Student(){ } //这样就将Student的显式原型指向了新创建出来的对象 Student.prototype = F(Person.prototype) //这样的继承方式,就不会有两份父类的属性
-
通过以上思想,也可以使用
Object.create()
来创建Object.create()方法创建的对象,需要手动指定该对象的隐式原型指向哪里
-
以下是实现的具体代码
function inherit(Subtype, Supertype) { //将子类的显式原型对象,指向新创建的对象 //该对象的隐式原型对象,指向的是父类的显式原型对象 Subtype.prototype = Object.create(Supertype.prototype); //给子类的显式原型对象,设置constructor Object.defineProperty(Subtype.prototype, "construtor", { enumerable: false, configurable: true, writable: true, value: Subtype, }); } //创建父类 function Person(name, age) { this.name = name; this.age = age; } //给父类添加方法 Person.prototype.running = function () { console.log("running"); }; //创建子类 function Student(name, age, classroom) { //借用构造函数的方式实现属性继承 Person.call(this, name, age); this.classroom = classroom; } //使子类继承父类 inherit(Student, Person); //给子类添加方法 //一定要写在继承之后!! Student.prototype.study = function () { console.log("studying"); }; let stu1 = new Student("zhangcheng", 18, 2002); console.log(stu1.name, stu1.age, stu1.classroom); stu1.running(); console.log(stu1); stu1.study();
Object是所有类的父类
-
函数对象最终也是继承自Object
Object.prototype.message = "zhangcheng" function foo(){} foo.message//zhangcheng
-
所以,我们创建出来的对象,可以使用toString等方法,是因为Object上面有这些方法
原型继承关系图(重要)
- 首先要明确:构造函数以函数角度看时有相应的显式原型(
prototype
);以对象角度(每个函数都可看成一个对象)有相应的 隐式原型(__proto__
) - 第二点:所有对象的隐式原型都指向**,创建它的,构造函数的显式原型**(Object.prototype的隐式原型除外,它指向null)
- 而构造函数的显式原型,本身就是对象,均由Object实例化出来的—>所以都指向Object的显式原型
- 第三点:Function/Object/Foo/所有函数都是Function的实例对象(均
new Function()
方式创建)- 因此所有函数对象的隐式原型均指向创建它的构造函数的显式原型
- 因此 Object是Function的父类,Function是Object的构造函数
构造函数的实例方法和类方法
- 实例方法:只能通过创建出来的实例进行调用
- 类方法::只能通过构造函数调用
//构造函数Person
function Person(name) {
this.name = name;
}
//只能通过p1调用:实例方法
Person.prototype.study = function () {
console.log("123");
};
//只能通过Person进行调用:类方法
Person.running = function () {
console.log("456");
};
let p1 = new Person();
p1.study();
Person.running();
对象方法补充
hasOwnProperty
-
判断某个属性,是否属于对象本身的(不是在原型上的属性)
function Person(name){ this.name = name } function Student(className){ this.className = className } //Student 继承自 Person let stu1 = new Student(2002) stu1.hasOwnProperty("className")//true stu1.hasOwnProperty("name")//false
in/for in
-
判断某个属性是否在某个对象上,或者原型上
function Person(name){ this.name = name } function Student(className){ this.className = className } //Student 继承自 Person let stu1 = new Student(2002) console.log("name" in stu1)//true console.log("className" in stu1)//true //注意,for in 遍历对象的时候,会将原型链上出现的属性都遍历出来 for(let key in stu1){ console.log(key) }
instanceof
-
用于检测 构造函数的显式原型,是否出现在 某个实例对象原型链上
-
也可以大致理解为,某个实例对象,是否是该构造函数创造出来的
-
这个原理就是
- 会顺着stu1的隐式原型去查找,看所对应的原型上的constructor是否返回了所写内容
- 我们指定原型对象上面的constructor返回的是构造函数本身
function Person(name){ this.name = name } function Student(className){ this.className = className } //Student 继承自 Person let stu1 = new Student(2002) console.log(stu1 instanceof Student)//true console.log(stu1 instanceof Person)//true
isPrototypeOf
- 用于检测某个对象,是否出现在某个实例对象的原型链上
- 也可以大致理解为:对象与对象之间的关系
- 用的比较少
原文链接:https://juejin.cn/post/7319297259644272674 作者:前端大菜鸟_