作者:娇娇jojo
时间:2019年4月26
一、类和对象
1、概念
提到类和对象,一下子解释清楚可能有点复杂,那就从生活中熟悉的东西入手。
在我脑海中立马闪现的一个词就是“人类”,那什么叫人类?女娲抟土造人,每个人都有五官、有血有肉,而且还会走路、说话、思考,而这一类有五官、有血有肉、会说话会走路会思考的物种,就叫做人类。
所以类的概念很简单,类(人类)就是对象(人)的模板,定义了同一组对象(人)共有的属性(五官、有血有肉)和方法(会走路、会说话、会思考)。
同时也引入了对象的概念,其中人类中的某个人就是对象,所以:
类,也叫做 class,是有相似属性的对象的抽象。
对象,也经常会说是 instance,是类具像化的一个实例(也叫做对象,下文可能我会混用这两个概念)。
2、使用类的原因
知道了类的概念,那我们现在就去了解一下为什么要去使用它。举个栗子,比如我要写 2 个背景色为红色的 div,你可能直接实例化 2 个对象:
<div style="background: red"></div> <div style="background: red"></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 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。