面向对象

面向对象我也不知道该从哪里开始说比较好,还是以《我的简历》 的代码为例子加以说明理解吧。

一、先修改下代码吧,不知道该用什么标题

讲真,每次想标题真是绞尽脑汁…

目前《我的简历》的代码目录结构如下图所示:

目录结构

其中,JS 文件夹中包含 init-swiper.js ,这个文件是用来初始化轮播图的 js , message.js 是用来处理留言信息的 js ,等等。其他就不一一的详细介绍。翻看其中代码发现每个 js 的代码结构都很类似,都分别包括视图层( view ) 、 控制层 ( controller ) 和 模型层 ( model ) 。由此可见其实每个文件的代码结构还是非常类似的,基于这种情况,我们决定尝试着给这几个 js 文件做一下类似封装的操作。

1、封装 View 层

我们在 js 目录下新建 base 文件夹,并新建文件 View.js ,其代码为:

1
2
3
window.View = function(selector){
return document.querySelector(selector);
}

在 index.html 中引入 View.js ,由于 View 方法直接绑定在了 window 上,所以在使用 MVC 结构写法的 js 代码中,可以将视图层变量 let view = document.querySelelctor(***) 改写为 View(***) 即可。 View 层封装完毕。

2、封装 Model 层

新建 ./js/base/Model.js 文件。其代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
window.Model = function(options){
let resourceName = options.resourceName;
return {
init : function(){
var APP_ID = '0r7UeXa1LVbwKLzTKIKvxueQ-gzGzoHsz';
var APP_KEY = 'ELk4KGSA1TR3oAd091pwWi9m';
AV.init({appId: APP_ID,appKey: APP_KEY});
},
fetch : function(){
//存数据
var query = new AV.Query(resourceName);
return query.find();
},
save:function(object){
//创建表
let Message = AV.Object.extend(resourceName);
//创建表中的一条数据
let message = new Message();
//存储一条数据到表中
return message.save(object)
}
}
}

model 层主要用于对数据的操作,即通过接口对数据进行增删改查。就这个简历项目而言,我们所有的数据操作都是多 LeanCloud 中的数据进行增、查操作,所以在进行数据相关操作时,需要将要操作的表名以 key : value 的参数形式传入调用中。以上,在 index.html 中引入此文件,并在需要实例化 Model 的地方改为: Model({resourceName : 'Message'}) ( 此处以 message.js ) 文件为例子。再依次修改其他文件即可。不可置否,这样定义、引用确实使代码清爽了很多。

3、封装 Controller 层

对比 resume 中的几个 js 可以发现, controller 不像 view 、 model 那样重复性比较高,所以在封装 controller 时我们只能提取出比较常见的部分,如属性 view 、 model 和常用方法 init() 、 bindEvents(),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.Controller = function(options){
let init = options.init;
this.bindEvents = options.bindEvents;
return {
view : null,
model : null,
init : function( view , model ){
this.view = view;
this.model = model || {};
if(model){this.model.init()};
init.call(this,view,model);
this.bindEvents();
}
}
}

init 、 bindEvents 事件通过 options 传递到 Controller 中,init 中大部分都会执行赋值属性 view 、 model 并且执行 model 的初始化、执行事件绑定。在此需要注意,有的 MVC 结构中不存在 model 层,所有在为属性 model 赋值和初始化 model 的时候加上了条件判断,这样在执行 js 时,如果没有 model 会自动跳过。

完成封装后,我们在 index.html 中引入此文件,然后就可以在各个 js 完美调用啦。拿 message.js 为例子进行修改,发现 controller 除了 init 和 bindEvents 还有其他的事件如 saveMessage 等,这些都需要通过定义的 Controller 的传参 options 传进来,那么此时针对封装的 Controller 需要进行改动一下,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
window.Controller = function(options){
let init = options.init;
this.bindEvents = options.bindEvents;
let object = {
view : null,
model : null,
init : function( view , model ){
this.view = view;
this.model = model || {};
if(model){this.model.init()};
init.call(this,view,model);
this.bindEvents();
}
}
for(let key in options){
if(key != 'init'){
object[key] = options[key]
}
}
return object;
}

按照以上代码,可将其他页面的 js 中 controller 进行封装。

二、一些关于 this 的问题

首先看一下这段代码:

1
2
3
4
5
6
7
8
9
button.onclick = function(){
console.log(this);
}
button.addEventListener('click',function(){
console.log(this)
}
$('ul').on('click','li',function(){
console.log(this);
})

以上三个 this 输出的各是什么?

1、onclick

针对 onclick 事件,我们可以看一下 MDN 官方文档:

onclick

this 值是触发事件的元素。由此可见,此处 this 指的是 button 。

2、addEventListener

addEventListener

此处 this 的值是触发事件元素的引用,也就是 button 。

3、$().on(‘click’)

on

此处 this 则代表了与 selector 相匹配的元素,那么针对 on 事件,此处的 this 指的就是 li 元素。

4、新的例子

1
2
3
4
button.onclick = function f1(){
console.log(this)
}
button.onclick.call({name:'summer'})

以上在执行的时候,由于 call 的第一个参数就是 this ,所以刚进入页面进行加载的时候就会输出对象 {name:’summer’} 。

5、难一点的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function X(){
return object = {
name:'object',
f1(x){
x.f2()
},
f2(){
console.log(this) //A
}
}
}

var options = {
name:'options',
f1(){},
f2(){
console.log(this) //B
}
}
var x = X();
x.f1(options)

问以上是 A 被调用还是 B 被调用?
分析:在代码 var x = X() 处可以得到 x 的值为:

1
2
3
4
5
6
7
8
9
x = {
name:'object',
f1(x){
x.f2()
},
f2(){
console.log(this)
}
}

接下来 x.f1(options) 为执行 x.f1() 并且将 options 作为参数传递进去,此时 f1() 中的 x 为 options ,那么 options 执行 f2() 会执行 options 下 f2 的方法,即打印 options 对象。即答案为 B 。

6、再来一个更深层次的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function X(){
return object = {
name:'object',
f1(x){
x.f2.call(this)
},
f2(){
console.log(this) //C
}
}
}

var options = {
name:'options',
f1(){},
f2(){
console.log(this) //D
}
}
var x = X();
x.f1(options)

接下来 x.f1(options) 为执行 x.f1() 并且将 options 作为参数传递进去,此时 f1() 中的 x 为 options ,此时的 this 就是当前的 x 也就是 object ,在执行 x.f2 时也就是在执行 options.f2 并将 this (object) 作为参数传递进去,那个输出的 this 就是 object ,即执行 options 中的 f2() 并且打印出的是 object 。

7、最后一个了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function X(){
return object = {
name:'object',
options:null,
f1(x){
this.options = x;
this.f2();
},
f2(){
this.options.f2.call(this)
}
}
}

var options = {
name:'options',
f1(){},
f2(){
console.log(this)
}
}
var x = X();
x.f1(options)

var x = X() ,此时 x 为 object ,执行 x.f1 并传入参数 options ,x.f1(x) 中的参数 x 为 options ,this 为调用此方法的对象,即变量 x ,将变量 x 的属性值 options 赋值为 传入的参数 options ,然后执行 this.f2() ,由于此时 this 为 x ,所以执行 this.options.f2.call(this) ,此处的两个 this 均指的是变量 x ,所以 this.options.f2 指的是 options 里面的 f2() ,传入的参数 this 为变量 x ,也就是 function X() 中返回的对象。

三、当我们在使用 new Object 或 new Array() 时 JS 都做了什么

这个 new 就是 new Object 和 new Array() 中的 new 。话说我刚看到这个标题的时候是在想这么简单的东西为什么还要讲,当需要声明一个新的对象或数组时,直接使用 new xxx() 不就行了么。然而啊,还是看看下面的例子吧。

比如说我有一个猫舍,喵妈生了一窝小喵,那么每一个小喵都有一些属性和行为特性,可以使用一个对象来表示:

1
2
3
4
5
6
7
8
9
10
let cat = {
color : 'white',
id : 1 ,
male : 0 ,
run : function(){},
play : function(){},
eat : function(){},
attack : function(){}
}
catMama.born(cat); //喵妈妈生了一只小喵

如果生了很多只猫的话,代码的结构可能就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let cat = {
color : 'white',
id : 1 ,
male : 0 ,
run : function(){},
play : function(){},
eat : function(){},
attack : function(){}
}
let cat1 = {...}
let cat2 = {...}
...
let catn = {...}
catMama.born(cat);
catMama.born(cat1);
catMama.born(cat2);
...
catMama.born(catn);

可以看出以上代码重复性非常高,所以此时我们可以尝试着使用循环来完成这个生小喵的过程,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let cats = [];
for(let i = 0;i<10;i++){
let cat = {
color : 'white',
id : i ,
male : 0 ,
run : function(){},
play : function(){},
eat : function(){},
attack : function(){}
}
cats.push(cat);
}
catMama.born(cats);

这么做看起来代码确实简洁了不少,但是我们通过内存图来了解下其中的弊端:

stack

如上图所示,每次生成一个新的变量 cat 都会在内存中占用一定的空间,用于存放此变量的属性和方法的地址,如图所示为红色 1~7 的部分。其中 4~7 用于存放方法的地址,此地址指向内存中写入的方法,如 run() 、 eat() 等。如果 for 循环中循环次数较少,对内存的占用还比较少,但是在实际业务中的大量操作,并且这几个方法的实现都一模一样,则会造成内存的极大浪费。

按照之前讲的 MVC 代码结构,重复即可简化的思想,这些重复定义的方法可以提取出来称为公共的方法。因为 cat 本身就是一个对象,那么我们可以写一个原型,使 cat 继承这个原型就可以啦。这样的话在每次生成 cat 对象的时候,其公共方法统统指向生成的这个原型,继承这个原型的 run() 、 play() 、eat() 、 attack() 方法。这个原型(共有方法)的具体定义和使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var cat_commom = {
type : 'animal',
run : function(){},
play : function(){},
eat : function(){},
attack : function(){}
}
let cats = [];
for(let i = 0;i<10;i++){
let cat = {
color : 'white',
id : i ,
male : 0
}
cat.__prototype__ = cat_common;
cats.push(cat);
}
catMama.born(cats);

写到这里,挑毛病的时间又到了。。。
变量 cat_common 和 cat 看起来好像没有什么关联,这样我们提取一下 cat_common 和 cat 到一个单独的文件为 cat.js ,其代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var cat_commom = {
type : 'animal',
run : function(){},
play : function(){},
eat : function(){},
attack : function(){}
}
function createcat(id){
var cat = {
id : id
}
cat.__prototype__ = cat_common;
return cat;
}

在引入 cat.js 文件后,在文件中的调用如下:

1
2
3
4
5
let cats = [];
for(let i = 0;i<10;i++){
cats.push(createCat(i));
}
catMama.born(cats);

在 cat.js 文件中,变量 cat_common 和函数 createCat() 仍旧是h没有什么关联,我们尝试着修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createCat(id){
var cat = {
id : id
}
cat.__prototype__ = createCat.common;
return cat;
}
createCat.common = {
type : 'animal',
run : function(){},
play : function(){},
eat : function(){},
attack : function(){}
}

以上代码, JS 之父其实早就为我们考虑到了,通常我们在使用 new Array() 时,其实代码执行的流程跟 cat.js 是类似的,JS 之父已经将里面的一些工作帮我们实现了,看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createcat(id){
// let temp = {};
// this = temp;
this.id = id;
// this.__prototype__ = createCat.common;
// return this;
}
createCat.common = {
type : 'animal',
run : function(){},
play : function(){},
eat : function(){},
attack : function(){}
}

其中,第二行、第三行、第五行、第六行 JS 之父已经为我们写好了,并且在第五行 .common 也已经统一命名为 prototype ,在定义一个对象是我们需要做的就是将对象的自有属性和原型定义好即可,然后在需要使用的地方 new xxx() 。

以上就是 new 实现的大致过程。

现在我们来看一下当我们写下 var object = new Object() 的时候,JS 都做了什么。
首先,先将自有属性传递进去,此处为空,然后就是进行原型的继承,即: object.prototype === Object.prototype 。

再看一个例子,当我们写下 var arr = new Array('a','b','c') 时,JS 都做了什么?
其中,arr 的自有属性为 0 : ‘a’ , 1 : ‘b’ , 2 : ‘c’ ,length : 3 , arr 的原型 arr.prototype === Array.prototype ,arr.prototype.prototype === Array.prototype.prototype === Object.prototype。

再看一个例子: var fn = new Function('x','y','return x+y') ,其中自有属性为 ‘x’ ,’y’ ,length 和不可见的函数体: return x+y , fn.prototype === Function.prototype ,Function.prototype.prototype === Object.prototype。