其实,不管是写什么语言的程序员,最终的目的,都是把产品或代码封装到一块儿,提供接口,让使用者很温馨的实现功能。因此对于我来讲,每每头疼的不是写代码,而是写注释和文档!若是接口很乱,确定会头疼一成天。javascript
JavaScript 最初是以 Web 脚本语言面向大众的,尽管如今出了服务器端的 nodejs,可是单线程的性质尚未变。对于一个 Web 开发人员来讲,能写一手漂亮的组件极为重要。GitHub 上那些开源且 stars 过百的 Web 项目或组件,可读性确定很是好。html
组件教程的参考来自于 GitHub 上,通俗易懂,连接。前端
要实现下面这个功能,对一个 input 输入框的内容进行验证,只有纯数字和字母的组合才是被接受的,其余都返回 failed:java
这种写法彻底没有约束,基本全部人都会,彻底没啥技巧:node
// html <input type="text" id="input"/> // javascript var input = document.getElementById("input"); function getValue(){ return input.value; } function render(){ var value = getValue(); if(!document.getElementById("show")){ var append = document.createElement('span'); append.setAttribute("id", "show"); input.parentNode.appendChild(append); } var show = document.getElementById("show"); if(/^[0-9a-zA-Z]+$/.exec(value)){ show.innerHTML = 'Pass!'; }else{ show.innerHTML = 'Failed!'; } } input.addEventListener('keyup', function(){ render(); });
缺点天然不用多说,变量没有任何隔离,严重污染全局变量,虽然能够达到目的,但极不推荐这种写法。git
鉴于以上写法的弊端,咱们用对象来隔离变量和函数:程序员
var obj = { input: null, // 初始化并提供入口调用方法 init: function(config){ this.input = document.getElementById(config.id); this.bind(); //链式调用 return this; }, // 绑定 bind: function(){ var self = this; this.input.addEventListener('keyup', function(){ self.render(); }); }, getValue: function(){ return this.input.value; }, render: function(){ var value = this.getValue(); if(!document.getElementById("show")){ var append = document.createElement('span'); append.setAttribute("id", "show"); input.parentNode.appendChild(append); } var show = document.getElementById("show"); if(/^[0-9a-zA-Z]+$/.exec(value)){ show.innerHTML = 'Pass!'; }else{ show.innerHTML = 'Failed!'; } } } window.onload = function(){ obj.init({id: "input"}); }
相对于开放式的写法,上面的这个方法就比较清晰了。有初始化,有内部函数和变量,还提供入口调用方法。es6
新手能实现上面的方法已经很不错了,还记得当初作百度前端学院题目的时候,基本就是用对象了。github
不过这种方法仍然有弊端。obj 对象中的方法都是公开的,并非私有的,其余人写的代码能够随意更改这些内容。当多人协做或代码量不少时,又会产生一系列问题。编程
var fun = (function(){ var _bind = function(obj){ obj.input.addEventListener('keyup', function(){ obj.render(); }); } var _getValue = function(obj){ return obj.input.value; } var InputFun = function(config){}; InputFun.prototype.init = function(config){ this.input = document.getElementById(config.id); _bind(this); return this; } InputFun.prototype.render = function(){ var value = _getValue(this); if(!document.getElementById("show")){ var append = document.createElement('span'); append.setAttribute("id", "show"); input.parentNode.appendChild(append); } var show = document.getElementById("show"); if(/^[0-9a-zA-Z]+$/.exec(value)){ show.innerHTML = 'Pass!'; }else{ show.innerHTML = 'Failed!'; } } return InputFun; })(); window.onload = function(){ new fun().init({id: 'input'}); }
函数闭包写法的好处都在自执行的闭包里,不会受到外面的影响,并且提供给外面的方法包括 init 和 render。好比咱们能够像 JQuery 那样,稍微对其改造一下:
var $ = function(id){ // 这样子就不用每次都 new 了 return new fun().init({'id': id}); } window.onload = function(){ $('input'); }
尚未涉及到原型,只是简单的闭包。
基本上,这已是一个合格的写法了。
虽然上面的方法以及够好了,可是咱们的目的,是为了使用面向对象。面向对象一直以来都是被认为最佳的编程方式,若是每一个人的代码风格都类似,维护、查看起来就很是的方便。
可是,我想在介绍面向对象以前,先来回忆一下 JS 中的继承(实现咱们放到最后再说)。
提到继承,我首先想到的就是用 new 来实现。仍是以例子为主吧,人->学生->小学生,在 JS 中有原型链这么一说,__proto__ 和 prototype ,对于原型链就不过多阐述,若是不懂的能够本身去查阅一些资料。
在这里,我仍是要说明一下 JS 中的 new 构造,好比 var student = new Person(name)
,实际上有三步操做:
var student = {}; student.__proto__ = Person.prototype; Person.call(student, name)
获得的 student 是一个对象,__proto__执行 Person 的 prototype,Person.call 至关于 constructor。
function Person(name){ this.name = name; } Person.prototype.Say = function(){ console.log(this.name + ' can say!'); } var ming = new Person("xiaoming"); console.log(ming.__proto__ == Person.prototype) //true new的第二步结果 console.log(ming.name) // 'xiaoming' new 的第三步结果 ming.Say() // 'xiaoming can say!' proto 向上追溯的结果
利用 __proto__ 属性的向上追溯,能够实现一个基于原型链的继承。
function Person(name){ this.name = name; } Person.prototype.Say = function(){ console.log(this.name + ' can say!'); } function Student(name){ Person.call(this, name); //Person 的属性赋值给 Student } Student.prototype = new Person(); //顺序不能反,要在最前面 Student.prototype.DoHomeWork = function(){ console.log(this.name + ' can do homework!'); } var ming = new Student("xiaoming"); ming.DoHomeWork(); //'xiaoming can do homework!' ming.Say(); //'xiaoming can say!'
大概刚认识原型链的时候,我也就只能写出这样的水平了,我以前的文章。
打开调试工具,看一下 ming 都有哪些东西:
ming name: "xiaoming" __proto__: Person DoHomeWork: () name: undefined //注意这里多了一个 name 属性 __proto__: Object Say: () constructor: Person(name) __proto__: Object
当调用 ming.Say()
的时候,恰好 ming.__proto__.__proto__
有这个属性,这就是链式调用的原理,一层一层向下寻找。
这就是最简单的继承了。
来看一看刚才那种作法的弊端。
没有实现传统面向对象该有的 super 方法来调用父类方法,链式和 super 方法相比仍是有必定缺陷的;
形成过多的原型属性(name),constructor 丢失(constructor 是一个很是重要的属性,MDN)。
由于链式是一层层向上寻找,知道找到为止,很明显 super 直接调用父类更具备优点。
// 多了原型属性 console.log(ming.__proto__) // {name: undefined}
为何会多一个 name,缘由是由于咱们执行了 Student.prototype = new Person();
,而 new 的第三步会执行一个 call 的函数,会使得 Student.prototype.name = undefined
,刚好 ming.__proto__
指向 Student 的 prototype,用了 new 是没法避免的。
// 少了 constructor console.log(ming.constructor == Person) //true console.log(ming.constructor == Student) // false
这也很奇怪,明明 ming 是继承与 Student,却返回 false,究其缘由,Student.prototype
的 constructor 方法丢失,向上找到了 Student.prototype.__proto__
的 constructor 方法。
再找缘由,这句话致使了 Student.prototype
的 constructor 方法丢失:
Student.prototype = new Person();
在这句话以前打一个断点,曾经是有的,只是被替换掉了:
找到了问题所在,如今来改进:
// fn 用来排除多余的属性(name) var fn = function(){}; fn.prototype = Person.prototype; Student.prototype = new fn(); // 从新添上 constructor 属性 Student.prototype.constructor = Student;
用上面的继承代码替换掉以前的 Student.prototype = new Person();
咱们不能每一次写代码的时候都这样写这么多行来继承吧,因此,于情于理,仍是来进行简单的包装:
function classInherit(subClass, parentClass){ var fn = function(){}; fn.prototype = parentClass.prototype; subClass.prototype = new fn(); subClass.prototype.constructor = subClass; } classInherit(Student, Person);
哈哈,所谓的包装,就是重抄一下代码。
上面的问题只是简单的解决了多余属性和 constructor 丢失的问题,而 super 问题仍然没有改进。
举个栗子,来看看 super 的重要,每一个人都会睡觉,sleep 函数是人的一个属性,学生分为小学生和大学生,小学生晚上 9 点睡觉,大学生 12 点睡觉,因而:
Person.prototype.Sleep = function(){ console.log('Sleep!'); } function E_Student(){}; //小学生 function C_Student(){}; //大学生 classInherit(E_Student, Person); classInherit(C_Student, Person); //重写 Sleep 方法 E_Student.prototype.Sleep = function(){ console.log('Sleep!'); console.log('Sleep at 9 clock'); } C_Student.prototype.Sleep = function(){ console.log('Sleep!'); console.log('Sleep at 12 clock'); }
对于 Sleep 方法,显得比较混乱,而咱们想要经过 super,直接调用父类的函数:
E_Student.prototype.Sleep = function(){ this._super(); //super 方法 console.log('Sleep at 9 clock'); } C_Student.prototype.Sleep = function(){ this._super(); //super 方法 console.log('Sleep at 12 clock'); }
不知道对 super 的理解正不正确,总感受怪怪的,欢迎指正!
来看下 JQuery 之父是如何 class 的面向对象,原文在这,源码以下。
/* Simple JavaScript Inheritance * By John Resig http://ejohn.org/ * MIT Licensed. */ // Inspired by base2 and Prototype (function(){ // initializing 开关很巧妙的来实现调用原型而不构造,还有回掉 var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; // The base Class implementation (does nothing) // 全局,this 指向 window,最大的父类 this.Class = function(){}; // Create a new Class that inherits from this class // 继承的入口 Class.extend = function(prop) { //保留当前类,通常是父类的原型 var _super = this.prototype; // Instantiate a base class (but only create the instance, // don't run the init constructor) //开关 用来使原型赋值时不调用真正的构成流程 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 //对函数判断,将属性套到子类上 prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn){ //用闭包来存储 return function() { var tmp = this._super; // Add a new ._super() method that is the same method // but on the super-class this._super = _super[name]; // The method only need to be bound temporarily, so we // remove it when we're done executing //实现同名调用 var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, prop[name]) : prop[name]; } // 要返回的子类 function Class() { // All construction is actually done in the init method if ( !initializing && this.init ) this.init.apply(this, arguments); } //前面介绍过的,继承 Class.prototype = prototype; Class.prototype.constructor = Class; Class.extend = arguments.callee; return Class; }; })();
这个时候就能够很轻松的实现面向对象,使用以下:
var Person = Class.extend({ init: function(name){ this.name = name; }, Say: function(name){ console.log(this.name + ' can Say!'); }, Sleep: function(){ console.log(this.name + ' can Sleep!'); } }); var Student = Person.extend({ init: function(name){ this._super('Student-' + name); }, Sleep: function(){ this._super(); console.log('And sleep early!'); }, DoHomeWork: function(){ console.log(this.name + ' can do homework!'); } }); var p = new Person('Li'); p.Say(); //'Li can Say!' p.Sleep(); //'Li can Sleep!' var ming = new Student('xiaoming'); ming.Say(); //'Student-xiaoming can Say!' ming.Sleep();//'Student-xiaoming can Sleep!' // 'And sleep early!' ming.DoHomeWork(); //'Student-xiaoming can do homework!'
除了 John Resig 的 super 方法,不少人都作了尝试,不过我以为 John Resig 的实现方式很是的妙,也比较贴近 super 方法,我本人也用源码调试了好几个小时,才勉强能理解。John Resig 的头脑真是使人佩服。
在 JS 中,class 从一开始就属于关键字,在 ES6 终于可使用 class 来定义类。好比:
class Point { constructor(x, y){ this.x = x; this.y = y; } toString(){ return '(' + this.x + ',' + this.y + ')'; } } var p = new Point(3, 4); console.log(p.toString()); //'(3,4)'
更多有关于 ES6 中类的使用请参考阮一峰老师的 Class基本语法。
其实 ES6 中的 class 只是写对象原型的时候更方便,更像面向对象,class 的功能 ES5 彻底能够作到,好比就上面的例子:
typeof Point; //'function' Point.prototype; /* |Object |--> constructor: function (x, y) |--> toString: function() |--> __proto__: Object */
和用 ES5 实现的真的没有什么差异,反而如今流行的一些库比 ES6 的 class 能带来更好的效益。
那么,说了这么多面向对象,如今回到最开始的那个组件的实现——如何用面向对象来实现。
仍是利用 John Resig 构造 class 的方法:
var JudgeInput = Class.extend({ init: function(config){ this.input = document.getElementById(config.id); this._bind(); }, _getValue: function(){ return this.input.value; }, _render: function(){ var value = this._getValue(); if(!document.getElementById("show")){ var append = document.createElement('span'); append.setAttribute("id", "show"); input.parentNode.appendChild(append); } var show = document.getElementById("show"); if(/^[0-9a-zA-Z]+$/.exec(value)){ show.innerHTML = 'Pass!'; }else{ show.innerHTML = 'Failed!'; } }, _bind: function(){ var self = this; self.input.addEventListener('keyup', function(){ self._render(); }); } }); window.onload = function(){ new JudgeInput({id: "input"}); }
可是,这样子,基本功能算是实现了,关键是很差扩展,没有面向对象的精髓。因此,针对目前的状况,咱们准备创建一个 Base
基类,init
表示初始化,render
函数表示渲染,bind
函数表示绑定,destory
用来销毁,同时 get
、set
方法提供得到和更改属性:
var Base = Class.extend({ init: function(config){ this._config = config; this.bind(); }, get: function(key){ return this._config[key]; }, set: function(key, value){ this._config[key] = value; }, bind: function(){ //之后构造 }, render: function(){ //之后构造 }, destory: function(){ //定义销毁方法 } });
基于这个 Base
,咱们修改 JudgeInput
以下:
var JudgeInput = Base.extend({ _getValue: function(){ return this.get('input').value; }, bind: function(){ var self = this; self.get('input').addEventListener('keyup', function(){ self.render(); }); }, render: function(){ var value = this._getValue(); if(!document.getElementById("show")){ var append = document.createElement('span'); append.setAttribute("id", "show"); input.parentNode.appendChild(append); } var show = document.getElementById("show"); if(/^[0-9a-zA-Z]+$/.exec(value)){ show.innerHTML = 'Pass!'; }else{ show.innerHTML = 'Failed!'; } } }); window.onload = function(){ new JudgeInput({input: document.getElementById("input")}); }
好比,咱们后期修改了判断条件,只有当长度为 5-10 的时候才会返回 success,这个时候能很快定位到 JudgeInput
的 render 函数:
render: function(){ var value = this._getValue(); if(!document.getElementById("show")){ var append = document.createElement('span'); append.setAttribute("id", "show"); input.parentNode.appendChild(append); } var show = document.getElementById("show"); //修改正则便可 if(/^[0-9a-zA-Z]{5,10}$/.exec(value)){ show.innerHTML = 'Pass!'; }else{ show.innerHTML = 'Failed!'; } }
以我目前的能力,只能理解到这里了。
从一个组件出发,一步一步爬坑,又跑去介绍 JS 中的面向对象,若是你能看到最后,那么你就可动手一步一步实现一个 JQuery 了,纯调侃。
关于一个组件的写法,从入门级到最终版本,一波三折,不只要考虑代码的实用性,还要兼顾后期维护。JS 中实现面向对象,刚接触 JS 的时候,我能用简单的原型链来实现,后来看了一些文章,发现了很多问题,在看 John Resig 的 Class,感触颇深。还好,如今目的是实现了,共勉!
制做组件的例子
javascript oo实现
John Resig: Simple JavaScript Inheritance
欢迎来我博客一块儿交流。
2016-11-13
经指正,已经将错误的 supper 改为 super。