Javascript 面向对象笔记

date
Mar 25, 2018
slug
javascript-oop
status
Published
tags
Javascript
summary
Javascript 中继承的详解以及几种不同的继承方式
type
Post
说到面向对象,大多数人都想到的是高级语言:c++、java,但是我认为对于一名coder来说不论什么语言,一定要有面向对象这种思想(封装、继承、多态),我们只需要用语言这个工具把思想表达出来即可。本文只讨论继承。

Javascript 创建对象

function Ball(name){
  this.name = name;
}
var basketball = new Ball('basketball');

console.log(basketball.__proto__);

/*  输出
    constructor: ƒ ball(name)
    arguments: null
    caller: null
    length: 1
    name: "ball"
    prototype: {constructor: ƒ}
    __proto__: ƒ ()
*/

console.log(ball.prototype)

/*  输出
    constructor: ƒ ball(name)
    arguments: null
    caller: null
    length: 1
    name: "ball"
    prototype: {constructor: ƒ}
    __proto__: ƒ ()
*/

console.log(basketball.__proto__ === ball.prototype) // true

new 关键字做了什么?

利用 new & 构造函数 创建新的对象。这个创建新对象的过程分为三步:
  • 声明新的变量 basketball
  • new 将新变量的 \_proto\_ 属性指向了构造函数(Ball)的 prototype 属性,这时内存为 basketball 分配了内存,其成为了对象。basketball._proto_ = ball.prototype
  • 利用 call 函数将新产生的对象 basketball 的 this 指向 ball。即绑定 this。

prototype/proto 是什么?

有的书上别别用显示原型/隐示原型来分别代 prototype/_proto_ 还有的用原型对象/对象原型。其实不论哪一种说法,代表的东西都是一样的。这里我们使用第二种说法。俩者区别如下表:
  • prototype: 指向函数的原型对象(包括拥有的变量与方法,constructor ,proto)只有函数拥有此属性
  • proto:指向构造器的原型对象;不论对象或者函数都有此属性

面向对象 — 继承

使用 call/apply

该方式采取的办法是将父对象的构造函数绑定在子对象上。具体如下:
function Ball() {
    this.general = "球类运动";
}

function Basketball(name, space) {
    Ball.apply(this, arguments);
    this.name = name;
    this.space = space;
}

let bb = new Basketball('耐克7号球', '室内')

console.log(bb.general) // 球类运动

原型链继承

使子类原型对象指向父类的原型对象以实现继承。具体如下:
function Ball() {
    this.general = "球类运动";
    this.ballprint = function() {
        console.log('ball');
    };
}

function Basketball(name, space) {
    this.name = name;
    this.space = space;
    this.print = function() {
        console.log('basketball');
    };
}

Basketball.prototype = new Ball();
let bb = new Basketball('耐克7号球', '室内');

console.log(bb.general) // 1、球类运动
console.log(bb.ballprint()) // 2、ball
console.log(bb.name) // 3、耐克7号球
console.log(bb.print()) // 4、basketball
console.log(Basketball.prototype == Ball.prototype) // 5、true
console.log(Basketball.prototype.__proto__ == Ball.prototype) // 6、true
如果子类与父类中的属性、方法同名那么结果怎样呢?结果如下:
function Ball() {
    this.name = "球类运动";
    this.print = function() {
        console.log('ball');
    };
}

function Basketball(name, space) {
    this.name = name;
    this.space = space;
    this.print = function() {
        console.log('basketball');
    };
}

Basketball.prototype = new Ball();
let bb = new Basketball('耐克7号球', '室内');

console.log(bb.name) // 1、耐克7号球
console.log(bb.print()) // 2、basketball
console.log(Basketball.prototype == Ball.prototype) // 3、true
console.log(Basketball.prototype.__proto__ == Ball.prototype) // 4、true
此时虽然 bb._proto_ = Basketball.prototype = Ball.prototype 但是同名采取的就近访问的原则,所以执行 Basketball 中的语句。而不会通过 proto 原型链去去上级父类寻找变量与方法

改进后的原型继承

因为上述方法会修改构造函数(因为prototype中包含构造器,当调用语句Basketball.prototype = new Ball()时构造器也被改为了 Ball),所以我们应该手动置回。具体如下:
function Ball() {
    this.name = "球类运动";
    this.print = function() {
        console.log('ball');
    };
}

function Basketball(name, space) {
    this.name = name;
    this.space = space;
    this.print = function() {
        console.log('basketball');
    };
}

Basketball.prototype = new Ball();
Basketball.prototype.constructor = Basketball;
let bb = new Basketball('耐克7号球', '室内');
这样即可。当然这样的继承方式是多占用了些内存,Basketball.prototype = new Ball()当然还有不占内存的方式,比如利用空对象作为中介的方式。创建了一个临时的对象,理解起来不难。具体请参考:
阮老师这里介绍的空对象方法,没什么问题。但是我觉得没有把临时对象使用完后手动置空的操作,自己加上即可。

class/extends 关键字实现继承

es6中引入了类的概念,用 class 关键字声明的函数作为对象模版。具体如下:
class Ball{
	constructor(name) {
		this.name = name
	}
	play() {
		console.log('Ball is: ', this.name)
	}
}

class Basketball extends Ball {
	constructor(name) {
		super(name)
		this.name = name
	}
	playb() {
		console.log('Basketball is: ', this.name)
	}
}

let bb = new Basketball('nikeball')
bb.play(); // Ball is: nikeball
bb.playb(); // Basketball is: nikeball
console.log(typeof Ball) // 1、function
console.log(typeof Basketball) // 2、function
console.log(bb.__proto__ == Basketball.prototype) // 3、true
console.log(Basketball.prototype)
/* 4、
constructor: class Basketball
playb: ƒ playb()
__proto__: Object
*/
由打印出的结果1&2我们可以看出,class 好像是包在 function 上的语法糖;由3慢慢确定了这一点;由4我们更加确定了这一点,而且结合前面说的改进原型继承的方式,还可以尝试分析 class 继承的关键步骤
Basketball.prototype = new Ball()

Basketball.prototype.constructor = Basketball

// playb() {...} 相当于:
Basketball.prototype.playb = function() {...}
使用 class 时一定要注意在使用 this 或者子类构造函数返回前,一定要在子类中使用 super 关键字调用父类的构造函数。说白了就是在子类中一定要使用 super 。

参考资料


© i7eo 2017 - 2022