1.原型
_proto_: 每个对象都会有隐式原型对象,它指向该对象的构造函数的prototype。
prototype: 只有函数对象才有,默认指向空对象。在prototype上添加属性或方法,该函数对应的实例都会获得改属性的使用权。
constructor: 原型对象里的 constructor 指向构造函数本身
function Computer(name, price) {
this.name = name;
this.price = price;
this.text=function(){
return `${name}:${price}`
}
}
const huawei = new Computer('huawei', 1200)
图解:
- huawei是构造函数Computer的实例,所以huawei的__proto__指向Computer的prototype,即:
huawei.__proto__===Computer.prototype // true
- 而构造函数中,Computer.prototype的constructor指向构造函数本身,即:
hauwei.__proto__.constructor === Computer // true
- Computer.prototype本身也是对象,因为所有的__proto__指向构造函数的prototype,所以Computer.prototype的原型指向对象的prototype,即:
hauwei.__proto__.__proto__ === Object.prototype // true
- Object.prototype最终指向null
总结:如何理解原型?
每个对象都有一个 __proto__ 属性,该属性指向自己的原型对象
每个构造函数都有一个 prototype 属性,该属性指向实例对象的原型对象
原型对象里的 constructor 指向构造函数本身
2.构造函数
2.1 什么是对象?为什么面向对象?
为什么面向对象 : 逻辑迁移更加灵活、代码复用性高、高度的模块化
对象:
- 对象是对于单个物体的简单抽象
- 对象是一个容器,封装了属性 & 方法
属性:对象的状态
方法:对象的行为
// 简单对象
const Course = {
teacher: 'a',
leader: 'b',
startCourse: function(name) {
return `开始${name}课`;
}
}
// 函数对象
function Course() {
this.teacher = 'a';
this.leader = 'b';
this.startCourse = function(name) {
return `开始${name}课`;
}
}
2.2 什么是构造函数
构造函数:表征了一类物体的共同特征,从而生成对象。js本质上并不是基于类,而是基于构造函数+原型链。
2.3 构造函数的特点
- 函数体内使用的this,指向所要生成的实例
- 生成对象用new来进行实例化
- 可以做初始化传参
2.3 new做了什么?
function Course() {};
const course = new Course();
- 创建了一个空对象,作为返回的对象实例
- 将生成空对象的原型对象指向了构造函数的prototype属性
- 将当前实例对象赋给了内部this
- 执行构造函数的初始化代码
追问1:使用构造函数,会有什么性能上的问题?
构造函数中的方法,会存在于每一个生成的实例里,重复的挂载其实是会导致资源浪费
function Cuorse(){
this.teacher='a';
this.course='web';
this.work=function(name){
return `开始${name}课`
}
}
const course1 = new Course('es6');
const course2 = new Course('OOP');
优化:
function Cuorse(){
this.teacher='a';
this.course='web';
}
// 方法挂载于prototype上
Course.prototype.work = function(name) {
return `开始${name}课`;
}
const course1 = new Course('es6');
const course2 = new Course('OOP');
追问2:构造函数不初始化可以使用吗?
不可以
function Cuorse(){
this.teacher='a';
this.course='web';
this.work=function(name){
return `开始${name}课`
}
}
追问3:如果项目需要使用,如何解决?
instanceOf():判断某个实例是否属于某构造函数;A instanceof B ,返回值为boolean类型,用来判断A是否是B的实例对象或者B子类的实例对象
function Cuorse(){
const _isClass = this instanceof Course;
if (!_isClass) {
return new Course();
}
this.teacher='a';
this.course='web';
this.work=function(name){
return `开始${name}课`
}
}
使用:
console.log(Course().name)
cosnt course1=Course()
3.原型链
访问一个对象的属性时,现在自身属性中查找,找到则返回;如果没找到,再沿着_proto_这条链向上查找,proto_指向他构造函数的prototype;prototype本身也是对象,会找到Object的_proto(即null),如果最终没找到,找不到属性返回undefined,找不到方法会报错。
4.prototype如何实现继承
js如何实现继承:现有构造函数Game,需要创建一个构造函数LOL,实现通过LOL新建的实例,可以在实例上同时继承LOL和Game的属性
实现:重写原型对象方式。将父对象的属性方法,作为子对象原型对象的属性和方法
// Game类
function Game() {
this.name = 'lol'
this.skin = ['s'];
}
Game.prototype.getName = function() {
return this.name;
}
// LOL类
function LOL() {}
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
const game = new LOL();
追问1:这个方法,原型链继承有什么缺点?
使用如上方法创建两个实例,改变实例1的skin,实例2的skin也会被改变。(玩家1给游戏充值皮肤,玩家2不花钱也能收到)并且,实例化子类时,无法向父类做传参。
如下:
function Game() {
this.name = 'lol';
this.skin = ['s'];
}
Game.prototype.getName = function() {
return this.name;
}
// LOL类
function LOL() {}
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
const game1 = new LOL();
const game2 = new LOL();
game1.skin.push('ss');
console.log(game1.skin)
console.log(game2.skin)
缺点:
- 父类属性一旦赋值给子类的原型属性,此时属性属于子类的共享属性了
- 实例化子类时,无法向父类做传参
追问2:如何解决上述问题
解决方法:
构造函数继承法:在子类构造函数内部调用父类构造函数。
重写原型对象方式的解决方法,将Game的属性放在LOL的prototype上。此次的方法,是将Game的属性放在LOl里。解决了共享属性的问题 + 子向父传参问题
function Game(arg) {
this.name = 'lol';
this.skin = ['s'];
}
Game.prototype.getName = function() {
return this.name;
}
// LOL类
// 通过call改变this指向,将Game的属性放在LOL中,arg可换为子类向父类传递的参数
// 用class如何实现,可使用super
function LOL(arg) {
Game.call(this, arg);
}
const game3 = new LOL('arg');
追问3:原型链上的共享方法无法被读取继承,如何解决?
解决方法:组合继承
追问2中的方法解决了子类传值与共享属性问题,但是新建的实例无法继承Game原型链上的方法。此次的方法,将Game再次放在LOl的prototype上。LOL本身找不到的属性,可以通过prototype访问Game的原型链。
function Game(arg) {
this.name = 'lol';
this.skin = ['s'];
}
Game.prototype.getName = function() {
return this.name;
}
// LOL类
function LOL(arg) {
Game.call(this, arg);
}
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
const game3 = new LOL();
追问4:组合继承就没有缺点么?无论何种场景都会调用两次父类构造函数(Game构造函数)
第一次:初始化子类原型时
第二次:子类调用函数内部call父类时
解决方案:
寄生组合继承
function Game(arg) {
this.name = 'lol';
this.skin = ['s'];
}
Game.prototype.getName = function() {
return this.name;
}
// LOL类
function LOL(arg) {
Game.call(this, arg);
}
//区别处:
LOL.prototype = Object.create(Game.prototype);
LOL.prototype.constructor = LOL;
区别处说明:用父类的原型对象把子类的原型对象覆盖掉,object.create 创建一个对象,Game.prototype 父类的原型对象
提高追问5:看起来完美解决了继承,但是如何实现多重继承?
现有构造函数Game,构造函数Store;需要创建一个构造函数LOl;要求通过LOL新建的实例可以访问到Game和Store的所有属性和原型链
Object.assign() 是对象的静态方法,可以用来复制对象的可枚举属性到目标对象,利用这个特性可以实现对象属性的合并。它将返回目标对象。
function Game(arg) {
this.name = 'lol';
this.skin = ['s'];
}
Game.prototype.getName = function() {
return this.name;
}
function Store() {
this.shop = 'steam';
}
Store.prototype.getPlatform = function() {
return this.shop;
}
function LOL(arg) {
Game.call(this, arg);
Store.call(this, arg);
}
LOL.prototype = Object.create(Game.prototype);
// LOL.prototype = Object.create(Store.prototype);
//使用上面一行代码实现会覆盖,丢失Game的prototype
Object.assign(LOL.prototype, Store.prototype);
LOL.prototype.constructor = LOL;
// LOL继承两类
const game3 = new LOL();