手记

类和继承

作者:娇娇jojo

时间:2019年4月26

一、类和对象

1、概念

提到类和对象,一下子解释清楚可能有点复杂,那就从生活中熟悉的东西入手。

在我脑海中立马闪现的一个词就是“人类”,那什么叫人类?女娲抟土造人,每个人都有五官、有血有肉,而且还会走路、说话、思考,而这一类有五官、有血有肉、会说话会走路会思考的物种,就叫做人类。

所以类的概念很简单,类(人类)就是对象(人)的模板,定义了同一组对象(人)共有的属性(五官、有血有肉)和方法(会走路、会说话、会思考)。

同时也引入了对象的概念,其中人类中的某个人就是对象,所以:

  • 类,也叫做 class,是有相似属性的对象的抽象。

  • 对象,也经常会说是 instance,是类具像化的一个实例(也叫做对象,下文可能我会混用这两个概念)。

2、使用类的原因

知道了类的概念,那我们现在就去了解一下为什么要去使用它。举个栗子,比如我要写 2 个背景色为红色的 div,你可能直接实例化 2 个对象:

<div ></div>
<div ></div>

上面的写法一点问题都没有,但如果我要写 10 个,100 个呢?复制粘贴?不存在的。

那怎么做呢?那就把相同属性抽出来,放在同一个地方,然后去引用。这时候就想到了 CSS 里的 class,我们也管它叫类(好像知道了点什么,JS 里也有类,那它俩有什么关系呢?先埋个小点,后续再说),所以上面的写法就可以用下面的方法去实现啦:

<div class='item'></div>
<div class='item'></div>
.item{
    background: red;
}

如果想再加一些共有的属性,比如宽高、位置,都可以继续加上:

.item{  
    background: red;  
    margin: auto;  
    width: 100px;  
    height: 100px;
}

所以结论也出来了,使用类的原因很简单,就是为了复用,以减少不必要的开发成本,并且更好的维护代码。

回过头来再说上面留下的小疑问,其实在 JS 里也是这样的,我们通过类把共同的一些属性和方法放在一块儿,以达到更好的复用。

3、怎么去表示类和对象

刚才我们展示了一下 CSS 里的类和对象,那我们在 JS 里要如何表示类和对象呢?JS 里表示类和对象的方式是这样的:

var Item = function(){}

var item1 = new Item();
var item2 = new Item();

这里用 Item(大写首字母)来表示类,小写首字母的这两个来表示对象,所以在这里,你们一定会感觉很诧异,类就是一个函数?函数就是一个类?答案是也对也不对,下面慢慢说。

(1)什么是构造函数

构造函数就是当我们去实例化一个类的时候,对这个将要被具象的实例做一些初始化的设置,去“构造”这个实例,比方说,人是一个抽象的概念,所以我们可以有一个叫“人”的类:

var Person = function(){}

这里的 function,就是 Person 的构造函数。比方说每个人都有一个名字 name,那么就这样写构造函数:

var Person = function(name){  
    this.name = name
}

其中 this 就指代了即将被初始化的那个对象,所以这时候我们可以通过这个构造函数来初始化一个Bob了:

var bob = new Person('Bob')

当然还可以初始化一个Mike:

var mike = new Person('Mike')

这样,我们就通过Person这个构造函数初始化了两个对象。

那么,我提一个疑问,为什么我们要这么复杂的实例化两个对象呢?为什么不直接这样就好了:

var bob = {  
    'name': 'Bob'
}

var mike = {  
    'name': 'Mike'
}

其实第二部分也提到了原因:复用。

(2)成员属性和成员方法

当我们把属性和方法都放到类里的时候,他们就叫做“成员属性(或成员变量)”和“成员方法(或成员函数)”,比方说还是刚才那个 Person 类,我们刚才有了一个成员属性叫 name:

var Person = function(name){  
    this.name = name
}

我们还可以添加一个成员方法 sayHello:

var Person = function(name){  
    this.name = name;
}

Person.prototype.sayHello = function(){  
    console.log('My name is:' + this.name);
}

在这里,你一定发现了成员方法放的地方不一样!是放在 Person.prototype里的!

什么时候要放在 this 下,什么时候放在 prototype下呢?

答案是:如果一个 属性/方法 是和具体实例有关的,比方说你的名字和我的名字,每个实例都不一样的,就放在this下,如果 属性/方法 和具体实例无关,每个实例拥有的是同样的一份东西的话,就放在 prototype 下。所以在这里,我们就把 sayHello 这个成员方法放在了 prototype 下,因为我俩的sayHello的行为都是同样的。

所以我们也可以把 this 下的叫做实例属性和实例方法,把 prototype 下的叫做类属性和类方法。(下面我们会大量用到这四个概念,麻烦多看两遍这句话)

二、继承

刚才我们说了类和实例的关系,我们可以把多个相似的实例中的共同的代码放到一块儿来维护,然后把这个共同的地儿叫做类。

那类和类之间的关系呢?比方说 Boy 这个类和 Person 这个类,他们之间如果不考虑共同的代码的话,也许就是这样写的:

var Person = function(name){  
    this.name = name;
}

Person.prototype.sayHello = function(){  
    console.log('My name is:' + this.name);
}
var Boy = function(name){  
    this.name = name
}

Boy.prototype.sayHello = function(){  
    console.log('My name is:' + this.name);
}

Boy.prototype.fightWith = function(other){  
    console.log('fight with ' + other);
}

很明显地,中间有很多重复的代码,所以这时候我们就需要用继承的方式把这些重复的代码“合并”一下,但是在说继承之前,我们要先说一个题外的小知识点:

call 和 apply 函数

call和apply函数之前和类、继承这些概念一点关系都没有的,他俩是很特别的一类函数,他们是函数上的函数。什么意思呢?

比方说我们之前会有一些“对象上的函数”

var apple = {  
    color: "red",  
    eat: function(){    
        console.log('It`s good!')
    }    
}

而call和apply这两个函数,是函数上的函数(在Javascript里面,属性和函数是同等地位的,所以属性可以有的东西,函数也可以有,函数有的东西,属性也可以有,这也就是我们常说的“函数是一等公民”)

call和apply函数的作用是:把该函数的作用对象做替换

举个栗子:

var apple = {  
    color: 'red'
}
    
var pear = {  
    color: 'cyan'
}

var fruits = function(){  
    console.log(this.color)
}

如果我们直接调用fruits()的话,这个函数的默认作用对象并没有color这个属性,所以就会输出 undefined,可是我们如果通过call来替换一下 作用对象呢?

fruits.call(apple);

就会输出 red

这也是大家经常说的,换了一下this,其实我不是很喜欢这样说,因为不是换了一下this指针,而是把函数作用在别人身上了。

那apply又是做啥的? 其实apply和call是一样的,只是在有参数的时候的用法不一样罢了:call是逐一传递,而apply是把所有的参数作为一个数组来传递:

fruits.call(target, arg1, arg2, arg3...)
fruits.apply(target, [arg1, arg2, arg3])

好了,小知识点结束,让我们一块儿回到继承里吧!

如果我们要把 Boy 继承自 Person,要怎么写呢?如下:

var Person = function(name){  
    this.name = name;
}

Person.prototype.sayHello = function(){  
    console.log('My name is:' + this.name);
}

var Boy = function(name){  
    Person.call(this, name)
}

Boy.prototype = new Person();
Boy.prototype.constructor = Boy;
Boy.prototype.fightWith = function(other){  
    console.log('smile at ' + other);
};

这里有三个特别的地方:

  • 在构造函数里多了一个 Person.call(this, name)

  • 在Boy所有的prototype被修改之前,实例化了一个Person对象作为它的prototype

  • 将Boy的constructor属性指回Boy

Person.call这里就是刚才的那个小知识点,首先Person是一个构造函数,他是用来“构造” Person对象的,所以在

Person.call(this, name)

之后,将要被实例化的Boy实例就先具有了所有的Person的属性,也就是name部分被初始化好了。

但这个时候所有的Person的类方法(sayHello)都还在Person上~ 还不在将要实例化的Boy对象上,所以这时候才需要有第二个特别的地方:

Boy.prototype = new Person()

这样做了之后,所有的Person的类方法都会被传递给Boy,原因如下:

首先如果我们不用这种方式来传递prototype上的方法,我们第一想法会如何传递呢?当然是直接赋值:

Boy.prototype = Person.prototype

看起来挺好的,但是这样会产生一个问题,如果我要继续修改Boy类,为Boy上增加一个类方法的话:

Boy.prototype.fightWith = function(){}

那么Person上的prototype也被修改了!因为他们是同一个对象

那我们能不能直接遍历Person的prototype,来逐一复制给Boy呢?可以,但不好。因为JS里有一个关键字是instanceof,用来判断一个对象是不是某个类的实例:

var bob = new Boy()
bob instanceof Boy // true
bob instanceof Person // true

如果直接复制的话,那在第二个判断里就会出问题了。

那么现在要解答为什么是

Boy.prototype = new Person()

这个写法的话,我们就需要先说说一个JS的继承机制:原型链

(在这里提示一下我们现在的逻辑链条,我们要解决的问题是 Person 的prototype如何“给”到Boy,现在不能直接把Person的prototype复制给Boy,因为会无法判断instanceof,然后JS通过原型链的机制一方面解决了prototype“给”的事情,另一方面解决了直接复制的问题)

第三部分代码的作用:

每个 prototype 都有一个 constructor 属性,它指向构造函数,并且每一个类实例也有一个 constructor 属性, 默认指向 prototype.constructor。虽然功能上使用没有出现什么问题,但是如果有使用到 bob.constructor 去做一些判断还是其他的操作, 会出现隐患。 bob明明是 从 Boy类实例化出来的, 但是 bob.constructor 却指向 Person, 这个会造成继承紊乱。因此需要手动纠正,也就是这句代码的作用。

三、原型链

所有的类对象都有一个prototype

所有的实例对象都有一个__proto__ 指向所对应的类对象的 prototype

也就是说,如果实例化一个对象

var bob = new Boy()

的话,那么就有

bob.__proto__ === Boy.prototype

而JS引擎在运行 bob.sayHello() 这个函数的时候,首先JS会寻找 bob 这个对象的“实例方法”,当然这里是找不到的,因为sayHello这个函数是一个类方法,不是一个实例方法,所以JS引擎会再去寻找类方法

bob.__proto__.sayHello()

所有的寻找方式都是这样,先找实例属性,再找类属性,先找实例方法,再找类方法

那么,如果类方法也找不到会怎么办?

那JS引擎会继续去找父类上的类方法,就像这样

bob.sayHello() //实例方法,没找到
bob.__proto__.sayHello() //Boy上的类方法,没找到
bob.__proto__.__proto__.sayHello() //Person上的类方法,找到!

如果一直向上找的话,最终就会找到Object的__proto__,这样就找到了结尾,如果还找不到对应的方法的话,这个函数就是undefined。

也正因为如此,所以我们可以解答为什么上面那一步是这样了:

Boy.prototype = new Person()

因为 new Person() 是一个实例,所以这个实例就有一个__proto__属性,所以现在 Boy 这个类里就是:

//先改写一下
var p = new Person();
Boy.prototype = p;

//然后就可以有这个属性了
Boy.prototype.__proto__ === p.__proto__ === Person.prototype

所以这时候如果我们实例化一个Boy对象,那么就有:

//实例化黄Bob
var bob = new Boy()

//然后就有
bob.__proto__ === Boy.prototype
bob.__proto__.__proto__ === Boy.prototype.__proto__ === Person.prototype

这样就完成了原型链的构造与寻找。因此就有

bob.fightWith('mike'); // fight with mike

最后附上原型链关系图:

以及 new 一个对象的过程:

new Boy{    
    var obj = {};    
    obj.__proto__ = Boy.prototype;    
    var result = Boy.call(obj,"Bob");    
    return typeof result === 'obj'? result : obj;
}
  • 创建一个空对象 obj;

  • 将新创建的空对象的隐式原型指向其构造函数的显示原型;

  • 使用 call 改变 this 的指向;

  • 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。


0人推荐
随时随地看视频
慕课网APP