比如我们要实现这样一个组件,就是一个输入框里面字数的计数。
为了更清楚的演示,下面全部使用jQuery作为基础语言库。
最简陋的写法
<!DOCTYPE html>
<html>
<head>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
</head>
<body>
<input type="text" id="J_input">
<script>
$(function(){
var input = $('#J_input');
//用来获取字数
function getNum(){
return input.val().length;
}
function render() {
var num = getNum();
//没有字数的容器就新建一个
if ($('#J_input_count').length == 0) {
input.after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
}
//监听事件
input.on('keyup',function(){
render();
});
//初始化,第一次渲染
render();
})
</script>
</body>
</html>
以上是一串面向过程的写法。各种变量混乱,没有很好的隔离作用域,当页面变的复杂的时候,会很难去维护。
于是我们会想到命名空间。就是把所以东西放在一个对象里。我们要调用的时候通过这个对象调用。用来实现全局变量只有一个。其他的变量都是这个对象内部的。
var textCount = {
input:null,
init:function(config){
this.input = $(config.id); // textCount.init({id:'#J_input'})的调用方式使得this指向textCount。
this.bind();
//这边范围对应的对象,可以实现链式调用
return this;
},
bind:function(){
var self = this;
this.input.on('keyup',function(){
//这里的this作为inputCount对象内bind方法的内部函数,在这个内部函数里this是
//指向全局,故将之前的this先存下来,便于在函数内部使用。
//就像setTimeout函数的参数执行this也指向全局一样
self.render();
});
},
getNum:function(){
return this.input.val().length;
},
//渲染元素
render:function(){
var num = this.getNum();
if ($('#J_input_count').length == 0) {
this.input.after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
}
}
$(function() {
//在domready后调用
textCount.init({id:'#J_input'}).render(); //链式调用,先初始化胡渲染。
})
上面的代码getNum,bind虽说没有污染全局变量。但是其他代码可以很随意的改动这些。本应该内部私有的就不要被外面访问到。当代码量特别特别多的时候,很容易出现变量重复,或被修改的问题。我们接着改动
利用匿名函数自执行返回值的形式形成的闭包
var TextCount = (function(){
//私有方法,外面将访问不到
var _bind = function(that){
that.input.on('keyup',function(){
that.render();
});
}
var _getNum = function(that){
return that.input.val().length;
}
var TextCountFun = function(config){
}
TextCountFun.prototype.init = function(config) {
this.input = $(config.id);
_bind(this);
return this;
};
TextCountFun.prototype.render = function() {
var num = _getNum(this);
if ($('#J_input_count').length == 0) {
this.input.after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
};
//返回构造函数
return TextCountFun;
})();
$(function() {
new TextCount().init({id:'#J_input'}).render();
})
这种写法,把所有的东西都包在了一个自动执行的闭包里面,所以不会受到外面的影响,并且只对外公开了TextCountFun构造函数,生成的对象只能访问到init,render方法。但是为什么要返回一个构造函数。直接返回render函数不可以吗。render函数里进行init初始化操作。这样的做法把init融合在render的做法就不推荐。函数的功能单一性被破坏。造成代码耦合。
面向对象
但是呢,当一个页面特别复杂,当我们需要的组件越来越多,当我们需要做一套组件。仅仅用这个就不行了。首先的问题就是,这种写法太灵活了,写单个组件还可以。如果我们需要做一套风格相近的组件,而且是多个人同时在写。那真的是噩梦。
在编程的圈子里,面向对象一直是被认为最佳的编写代码方式。比如java,就是因为把面向对象发挥到了极致,所以多个人写出来的代码都很接近,维护也很方便。js中没有类Class。实现继承是靠原型链。我们可以用原型链封装出一个类似的Class。真正的oo语言,实现继承是靠复制。传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。js通过混入mixin来模仿类。并且实现多继承。
mixin
(function(){
//initializing是为了解决我们之前说的继承导致原型有多余参数的问题。
//当我们直接将父类的实例赋值给子类原型时。是会调用一次父类的构造函数的。所以这边会把真正的构造流程放到init函数里面,通过initializing来表示当前是不是处于构造原型阶段,
//为true的话就不会调用init。
//fnTest用来匹配代码里面有没有使用super关键字。对于一些浏览器`function(){xyz;}`会生成个字符串,并且会把里面的代码弄出来,有的浏览器就不会。
//`/xyz/.test(function(){xyz;})`为true代表浏览器支持看到函数的内部代码,所以用`/\b_super\b/`来匹配。如果不行,就不管三七二十一。
//所有的函数都算有super关键字,于是就是个必定匹配的正则。
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
// 超级父类
this.Class = function(){};
// Create a new Class that inherits from this class
// 生成一个类,这个类会具有extend方法用于继续继承下去
Class.extend = function(prop) {// 这里的extend很像mixin
//保留当前类,一般是父类的原型
//this指向父类。初次时指向Class超级父类
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
//开关 用来使原型赋值时不调用真正的构成流程。就是执行var p = Class.extend()时候不执行init。而是执行 new p()和Person.extend()时候执行init。
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
//这边其实就是很简单的将prop的属性混入到子类的原型上。如果是函数我们就要做一些特殊处理
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
//通过闭包,返回一个新的操作函数.在外面包一层,这样我们可以做些额外的处理
return function() {
var tmp = this._super;// 这个this是实例化对象的。var p = new Person(true);就是p的
// Add a new ._super() method that is the same method
// but on the super-class
// 调用一个函数时,会给this注入一个_super方法用来调用父类的同名方法
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
//因为上面的赋值,是的这边的fn里面可以通过_super调用到父类同名方法
var ret = fn.apply(this, arguments);
//离开时 保存现场环境,恢复值。
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
// 这边是返回的类,其实就是我们返回的子类
function Class1() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
// 赋值原型链,完成继承
Class1.prototype = prototype;
// 改变constructor引用
Class1.prototype.constructor = Class1;
// 为子类也添加extend方法
Class1.extend = arguments.callee;
return Class1;
};
})();
使用方法
var Person = Class.extend({
init: function(isDancing){
this.dancing = isDancing;
},
dance: function(){
return this.dancing;
}
});
var Ninja = Person.extend({
init: function(){
this._super( false );
},
dance: function(){
// Call the inherited version of dance()
return this._super();
},
swingSword: function(){
return true;
}
});
var p = new Person(true);
p.dance(); // => true
var n = new Ninja();
n.dance(); // => false
n.swingSword(); // => true
// Should all be true
p instanceof Person && p instanceof Class &&
n instanceof Ninja && n instanceof Person && n instanceof Class
javascript oo实现
有了这个类的扩展,我们可以这么编写代码了:
var TextCount = Class.extend({
init:function(config){
this.input = $(config.id);
this._bind();
this.render();
},
render:function() {
var num = this._getNum();
if ($('#J_input_count').length == 0) {
this.input.after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
},
_getNum:function(){
return this.input.val().length;
},
_bind:function(){
var self = this;
self.input.on('keyup',function(){
self.render();
});
}
})
$(function() {
new TextCount({
id:"#J_input"
});
})
抽象出base
可以看到,我们的组件有些方法,是大部分组件都会有的。
- 比如init用来初始化属性。
- 比如render用来处理渲染的逻辑。
- 比如bind用来处理事件的绑定。
当然这也是一种约定俗成的规范了。如果大家全部按照这种风格来写代码,开发大规模组件库就变得更加规范,相互之间配合也更容易。
这个时候面向对象的好处就来了,我们抽象出一个Base类。其他组件编写时都继承它。
var Base = Class.extend({
init:function(config){
//自动保存配置项
this.__config = config
this.bind()
this.render()
},
//可以使用get来获取配置项
get:function(key){
return this.__config[key]
},
//可以使用set来设置配置项
set:function(key,value){
this.__config[key] = value
},
bind:function(){
},
render:function() {
},
//定义销毁的方法,一些收尾工作都应该在这里
destroy:function(){
}
})
base类主要把组件的一般性内容都提取了出来,这样我们编写组件时可以直接继承base类,覆盖里面的bind和render方法。
于是我们可以这么写代码:
var TextCount = Base.extend({
_getNum:function(){
return this.get('input').val().length;
},
bind:function(){
var self = this;
self.get('input').on('keyup',function(){
self.render();
});
},
render:function() {
var num = this._getNum();
if ($('#J_input_count').length == 0) {
this.get('input').after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
}
})
$(function() {
new TextCount({
//这边直接传input的节点了,因为属性的赋值都是自动的。
input:$("#J_input")
});
})
网友评论