Javascript面向对象笔记

| 字数 1473
  1. 1. Javascript 中创建对象
    1. 1.1. new 关键字做了什么?
    2. 1.2. prototype/_proto_ 是什么?
  2. 2. 面向对象 — 继承
    1. 2.1. 1、call/apply
    2. 2.2. 2、原型链继承
    3. 2.3. 3、改进后的原型继承
    4. 2.4. 4、class/extends 关键字实现继承
  1. 1. Javascript 中创建对象
    1. 1.1. new 关键字做了什么?
    2. 1.2. prototype/_proto_ 是什么?
  2. 2. 面向对象 — 继承
    1. 2.1. 1、call/apply
    2. 2.2. 2、原型链继承
    3. 2.3. 3、改进后的原型继承
    4. 2.4. 4、class/extends 关键字实现继承

        说到面向对象,大多数人都想到的是高级语言:c++、java,但是我认为对于一名coder来说不论什么语言,一定要有面向对象这种思想(封装、继承、多态),我们只需要用语言这个工具把思想表达出来即可。本文只讨论继承。

Javascript 中创建对象

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_ 指向构造器的原型对象 不论对象或者函数都有此属性

来看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 创建对象过程一致。

面向对象 — 继承

1、call/apply

该方式采取的办法是将父对象的构造函数绑定在子对象上。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
// 球类运动

2、原型链继承

使子类原型对象指向父类的原型对象以实现继承。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

调试语句5更深层次的意思是说:

1
Basketball.prototype.constuctor = Ball.prototype.constuctor

因为前面说过每个函数特有 prototype 这个原型对象属性,而在这个原型对象中存有 constuctor ,所以要使俩函数的 prototype 相等,那么其中的 constuctor 必定也想等。所以 bb 作为子类才可以访问到 ball 中的属性与方法。语句1&2证明了这一点。

语句6结果符合 new 操作符的操作。

如果子类与父类中的属性、方法同名那么结果怎样呢?结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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_ 原型链去去上级父类寻找变量与方法

3、改进后的原型继承

因为上述2中的方法会修改构造函数,所以我们应该手动置回。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();当然还有不占内存的方式,比如利用空对象作为中介的方式。创建了一个临时的对象,理解起来不难。具体请参考:

Javascript面向对象编程(二):构造函数的继承

阮老师这里介绍的空对象方法,没什么问题。但是我觉得没有把临时对象使用完后手动置空的操作,自己加上即可。

4、class/extends 关键字实现继承

es6中引入了类的概念,用 class 关键字声明的函数作为对象模版。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 继承的关键步骤

1
2
3
4
5
6
Basketball.prototype = new Ball()

Basketball.prototype.constructor = Basketball

// playb() {...} 相当于:
Basketball.prototype.playb = function() {...}

使用 class 时一定要注意在使用 this 或者子类构造函数返回前,一定要在子类中使用 super 关键字调用父类的构造函数。说白了就是在子类中一定要使用 super 。

参考资料:

JS当中的new关键字都干了些什么?

作用域链与原型链

详解prototype与_proto_