从一个组件的实现来深刻理解JS中的继承

简介:

其实,无论是写什么语言的程序员,最终的目的,都是把产品或代码封装到一起,提供接口,让使用者很舒适的实现功能。所以对于我来说,往往头疼的不是写代码,而是写注释和文档!如果接口很乱,肯定会头疼一整天。

JavaScript 最初是以 Web 脚本语言面向大众的,尽管现在出了服务器端的 nodejs,但是单线程的性质还没有变。对于一个 Web 开发人员来说,能写一手漂亮的组件极为重要。GitHub 上那些开源且 stars 过百的 Web 项目或组件,可读性肯定非常好。

从一个例子来学习写组件

组件教程的参考来自于 GitHub 上,通俗易懂,链接。

要实现下面这个功能,对一个 input 输入框的内容进行验证,只有纯数字和字母的组合才是被接受的,其他都返回 failed:

全局变量写法

这种写法完全没有约束,基本所有人都会,完全没啥技巧:


 
 
  1. // html 
  2. <input type="text" id="input"/> 
  3. // javascript 
  4. var input = document.getElementById("input"); 
  5. function getValue(){ 
  6.   return input.value; 
  7. function render(){ 
  8.   var value = getValue(); 
  9.   if(!document.getElementById("show")){ 
  10.     var append = document.createElement('span'); 
  11.     append.setAttribute("id""show"); 
  12.     input.parentNode.appendChild(append); 
  13.   } 
  14.   var show = document.getElementById("show"); 
  15.   if(/^[0-9a-zA-Z]+$/.exec(value)){ 
  16.     show.innerHTML = 'Pass!'
  17.   }else
  18.     show.innerHTML = 'Failed!'
  19.   } 
  20. input.addEventListener('keyup'function(){ 
  21.   render(); 
  22. });  

缺点自然不用多说,变量没有任何隔离,严重污染全局变量,虽然可以达到目的,但极不推荐这种写法。

对象隔离作用域

鉴于以上写法的弊端,我们用对象来隔离变量和函数:


 
 
  1. var obj = { 
  2.   input: null
  3.   // 初始化并提供入口调用方法 
  4.   init: function(config){ 
  5.     this.input = document.getElementById(config.id); 
  6.     this.bind(); 
  7.     //链式调用 
  8.     return this; 
  9.   }, 
  10.   // 绑定 
  11.   bind: function(){ 
  12.     var self = this; 
  13.     this.input.addEventListener('keyup'function(){ 
  14.       self.render(); 
  15.     }); 
  16.   }, 
  17.   getValue: function(){ 
  18.     return this.input.value; 
  19.   }, 
  20.   render: function(){ 
  21.     var value = this.getValue(); 
  22.     if(!document.getElementById("show")){ 
  23.       var append = document.createElement('span'); 
  24.       append.setAttribute("id""show"); 
  25.       input.parentNode.appendChild(append); 
  26.     } 
  27.     var show = document.getElementById("show"); 
  28.     if(/^[0-9a-zA-Z]+$/.exec(value)){ 
  29.       show.innerHTML = 'Pass!'
  30.     }else
  31.       show.innerHTML = 'Failed!'
  32.     } 
  33.   } 
  34. window.onload = function(){ 
  35.   obj.init({id: "input"}); 
  36. }  

相对于开放式的写法,上面的这个方法就比较清晰了。有初始化,有内部函数和变量,还提供入口调用方法。

新手能实现上面的方法已经很不错了,还记得当初做百度前端学院题目的时候,基本就是用对象了。

不过这种方法仍然有弊端。obj 对象中的方法都是公开的,并不是私有的,其他人写的代码可以随意更改这些内容。当多人协作或代码量很多时,又会产生一系列问题。

函数闭包的写法


 
 
  1. var fun = (function(){ 
  2.   var _bind = function(obj){ 
  3.     obj.input.addEventListener('keyup'function(){ 
  4.       obj.render(); 
  5.     }); 
  6.   } 
  7.   var _getValue = function(obj){ 
  8.     return obj.input.value; 
  9.   } 
  10.   var InputFun = function(config){}; 
  11.   InputFun.prototype.init = function(config){ 
  12.     this.input = document.getElementById(config.id); 
  13.     _bind(this); 
  14.     return this; 
  15.   } 
  16.   InputFun.prototype.render = function(){ 
  17.     var value = _getValue(this); 
  18.     if(!document.getElementById("show")){ 
  19.       var append = document.createElement('span'); 
  20.       append.setAttribute("id""show"); 
  21.       input.parentNode.appendChild(append); 
  22.     } 
  23.     var show = document.getElementById("show"); 
  24.     if(/^[0-9a-zA-Z]+$/.exec(value)){ 
  25.       show.innerHTML = 'Pass!'
  26.     }else
  27.       show.innerHTML = 'Failed!'
  28.     } 
  29.   } 
  30.   return InputFun; 
  31. })(); 
  32. window.onload = function(){ 
  33.   new fun().init({id: 'input'}); 
  34. }  

函数闭包写法的好处都在自执行的闭包里,不会受到外面的影响,而且提供给外面的方法包括 init 和 render。比如我们可以像 JQuery 那样,稍微对其改造一下:


 
 
  1. var $ = function(id){ 
  2.   // 这样子就不用每次都 new 了 
  3.   return new fun().init({'id': id}); 
  4. window.onload = function(){ 
  5.   $('input'); 
  6. }  

还没有涉及到原型,只是简单的闭包。

基本上,这已经是一个合格的写法了。

面向对象

虽然上面的方法以及够好了,但是我们的目的,是为了使用面向对象。面向对象一直以来都是被认为最佳的编程方式,如果每个人的代码风格都相似,维护、查看起来就非常的方便。

但是,我想在介绍面向对象之前,先来回忆一下 JS 中的继承(实现我们放到最后再说)。

入门级的面向对象

提到继承,我首先想到的就是用 new 来实现。还是以例子为主吧,人->学生->小学生,在 JS 中有原型链这么一说,__proto__ 和 prototype ,对于原型链就不过多阐述,如果不懂的可以自己去查阅一些资料。

在这里,我还是要说明一下 JS 中的 new 构造,比如 var student = new Person(name),实际上有三步操作:


 
 
  1. var student = {}; 
  2. student.__proto__ = Person.prototype; 
  3. Person.call(student, name)  

得到的 student 是一个对象,__proto__执行 Person 的 prototype,Person.call 相当于 constructor。


 
 
  1. function Person(name){ 
  2.   this.name = name
  3. Person.prototype.Say = function(){ 
  4.   console.log(this.name + ' can say!'); 
  5. var ming = new Person("xiaoming"); 
  6. console.log(ming.__proto__ == Person.prototype) //true new的第二步结果 
  7. console.log(ming.name) // 'xiaoming' new 的第三步结果 
  8. ming.Say() // 'xiaoming can say!' proto 向上追溯的结果  

利用 __proto__ 属性的向上追溯,可以实现一个基于原型链的继承。


 
 
  1. function Person(name){ 
  2.   this.name = name
  3. Person.prototype.Say = function(){ 
  4.   console.log(this.name + ' can say!'); 
  5. function Student(name){ 
  6.   Person.call(this, name); //Person 的属性赋值给 Student 
  7. Student.prototype = new Person(); //顺序不能反,要在最前面 
  8. Student.prototype.DoHomeWork = function(){ 
  9.   console.log(this.name + ' can do homework!'); 
  10. var ming = new Student("xiaoming"); 
  11. ming.DoHomeWork(); //'xiaoming can do homework!' 
  12. ming.Say(); //'xiaoming can say!'  

大概刚认识原型链的时候,我也就只能写出这样的水平了,我之前的文章。

打开调试工具,看一下 ming 都有哪些东西:


 
 
  1. ming 
  2.   name"xiaoming" 
  3.   __proto__: Person 
  4.     DoHomeWork: () 
  5.     name: undefined //注意这里多了一个 name 属性 
  6.     __proto__: Object 
  7.       Say: () 
  8.       constructor: Person(name
  9.       __proto__: Object  

当调用 ming.Say() 的时候,刚好 ming.__proto__.__proto__ 有这个属性,这就是链式调用的原理,一层一层向下寻找。

这就是最简单的继承了。

面向对象的进阶

来看一看刚才那种做法的弊端。

  1. 没有实现传统面向对象该有的 super 方法来调用父类方法,链式和 super 方法相比还是有一定缺陷的;
  2. 造成过多的原型属性(name),constructor 丢失(constructor 是一个非常重要的属性,MDN)。

因为链式是一层层向上寻找,知道找到为止,很明显 super 直接调用父类更具有优势。


 
 
  1. // 多了原型属性 
  2. console.log(ming.__proto__) // {name: undefined}  

为什么会多一个 name,原因是因为我们执行了 Student.prototype = new Person();,而 new 的第三步会执行一个 call 的函数,会使得 Student.prototype.name = undefined,恰好 ming.__proto__ 指向 Student 的 prototype,用了 new 是无法避免的。


 
 
  1. // 少了 constructor 
  2.  
  3. console.log(ming.constructor == Person) //true 
  4.  
  5. console.log(ming.constructor == Student) // false  

这也很奇怪,明明 ming 是继承与 Student,却返回 false,究其原因,Student.prototype 的 constructor 方法丢失,向上找到了Student.prototype.__proto__ 的 constructor 方法。

再找原因,这句话导致了 Student.prototype 的 constructor 方法丢失:


 
 
  1. Student.prototype = new Person(); 

在这句话之前打一个断点,曾经是有的,只是被替换掉了:

找到了问题所在,现在来改进:


 
 
  1. // fn 用来排除多余的属性(name
  2. var fn = function(){}; 
  3. fn.prototype = Person.prototype; 
  4. Student.prototype = new fn(); 
  5. // 重新添上 constructor 属性 
  6. Student.prototype.constructor = Student;  

用上面的继承代码替换掉之前的 Student.prototype = new Person();

面向对象的封装

我们不能每一次写代码的时候都这样写这么多行来继承吧,所以,于情于理,还是来进行简单的包装:


 
 
  1. function classInherit(subClass, parentClass){ 
  2.   var fn = function(){}; 
  3.   fn.prototype = parentClass.prototype; 
  4.   subClass.prototype = new fn(); 
  5.   subClass.prototype.constructor = subClass; 
  6. classInherit(Student, Person);  

哈哈,所谓的包装,就是重抄一下代码。

进一步完善面向对象

上面的问题只是简单的解决了多余属性和 constructor 丢失的问题,而 supper 问题仍然没有改进。

举个栗子,来看看 supper 的重要,每个人都会睡觉,sleep 函数是人的一个属性,学生分为小学生和大学生,小学生晚上 9 点睡觉,大学生 12 点睡觉,于是:


 
 
  1. Person.prototype.Sleep = function(){ 
  2.   console.log('Sleep!'); 
  3. function E_Student(){}; //小学生 
  4. function C_Student(){}; //大学生 
  5. classInherit(E_Student, Person); 
  6. classInherit(C_Student, Person); 
  7. //重写 Sleep 方法 
  8. E_Student.prototype.Sleep = function(){ 
  9.   console.log('Sleep!'); 
  10.   console.log('Sleep at 9 clock'); 
  11. C_Student.prototype.Sleep = function(){ 
  12.   console.log('Sleep!'); 
  13.   console.log('Sleep at 12 clock'); 
  14. }  

对于 Sleep 方法,显得比较混乱,而我们想要通过 supper,直接调用父类的函数:


 
 
  1. E_Student.prototype.Sleep = function(){ 
  2.   this._supper(); //supper 方法 
  3.   console.log('Sleep at 9 clock'); 
  4. C_Student.prototype.Sleep = function(){ 
  5.   this._supper(); //supper 方法 
  6.   console.log('Sleep at 12 clock'); 
  7. }  

不知道对 supper 的理解正不正确,总感觉怪怪的,欢迎指正!

来看下 JQuery 之父是如何 class 的面向对象,原文在这,源码如下。


 
 
  1. /* Simple JavaScript Inheritance 
  2.  * By John Resig http://ejohn.org/ 
  3.  * MIT Licensed. 
  4.  */ 
  5. // Inspired by base2 and Prototype 
  6. (function(){ 
  7.   // initializing 开关很巧妙的来实现调用原型而不构造,还有回掉 
  8.   var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; 
  9.   // The base Class implementation (does nothing) 
  10.   // 全局,this 指向 window,最大的父类 
  11.   this.Class = function(){}; 
  12.   
  13.   // Create a new Class that inherits from this class 
  14.   // 继承的入口 
  15.   Class.extend = function(prop) { 
  16.     //保留当前类,一般是父类的原型 
  17.     var _super = this.prototype; 
  18.     
  19.     // Instantiate a base class (but only create the instance, 
  20.     // don't run the init constructor) 
  21.     //开关 用来使原型赋值时不调用真正的构成流程 
  22.     initializing = true
  23.     var prototype = new this(); 
  24.     initializing = false
  25.     
  26.     // Copy the properties over onto the new prototype 
  27.     for (var name in prop) { 
  28.       // Check if we're overwriting an existing function 
  29.       //对函数判断,将属性套到子类上 
  30.       prototype[name] = typeof prop[name] == "function" && 
  31.         typeof _super[name] == "function" && fnTest.test(prop[name]) ? 
  32.         (function(name, fn){ 
  33.           //用闭包来存储 
  34.           return function() { 
  35.             var tmp = this._super; 
  36.             
  37.             // Add a new ._super() method that is the same method 
  38.             // but on the super-class 
  39.             this._super = _super[name]; 
  40.             
  41.             // The method only need to be bound temporarily, so we 
  42.             // remove it when we're done executing 
  43.             //实现同名调用 
  44.             var ret = fn.apply(this, arguments);   
  45.             this._super = tmp; 
  46.             return ret; 
  47.           }; 
  48.         })(name, prop[name]) : 
  49.         prop[name]; 
  50.     } 
  51.     
  52.     // 要返回的子类 
  53.     function Class() { 
  54.       // All construction is actually done in the init method 
  55.       if ( !initializing && this.init ) 
  56.         this.init.apply(this, arguments); 
  57.     } 
  58.     //前面介绍过的,继承 
  59.     Class.prototype = prototype; 
  60.     
  61.     Class.prototype.constructor = Class; 
  62.   
  63.     Class.extend = arguments.callee; 
  64.     
  65.     return Class; 
  66.   }; 
  67. })();  

这个时候就可以很轻松的实现面向对象,使用如下:


 
 
  1. var Person = Class.extend({ 
  2.   init: function(name){ 
  3.     this.name = name
  4.   }, 
  5.   Say: function(name){ 
  6.     console.log(this.name + ' can Say!'); 
  7.   }, 
  8.   Sleep: function(){ 
  9.     console.log(this.name + ' can Sleep!'); 
  10.   } 
  11. }); 
  12. var Student = Person.extend({ 
  13.   init: function(name){ 
  14.     this._super('Student-' + name); 
  15.   }, 
  16.   Sleep: function(){ 
  17.     this._super(); 
  18.     console.log('And sleep early!'); 
  19.   }, 
  20.   DoHomeWork: function(){ 
  21.     console.log(this.name + ' can do homework!'); 
  22.   } 
  23. }); 
  24. var p = new Person('Li'); 
  25. p.Say(); //'Li can Say!' 
  26. p.Sleep(); //'Li can Sleep!' 
  27. var ming = new Student('xiaoming'); 
  28. ming.Say(); //'Student-xiaoming can Say!' 
  29. ming.Sleep();//'Student-xiaoming can Sleep!' 
  30.             // 'And sleep early!' 
  31. ming.DoHomeWork(); //'Student-xiaoming can do homework!'  

除了 John Resig 的 supper 方法,很多人都做了尝试,不过我觉得 John Resig 的实现方式非常的妙,也比较贴近 supper 方法,我本人也用源码调试了好几个小时,才勉强能理解。John Resig 的头脑真是令人佩服。

ES6 中的 class

在 JS 中,class 从一开始就属于关键字,在 ES6 终于可以使用 class 来定义类。比如:


 
 
  1. class Point { 
  2.   constructor(x, y){ 
  3.     this.x = x; 
  4.     this.y = y; 
  5.   } 
  6.   toString(){ 
  7.     return '(' + this.x + ',' + this.y + ')'
  8.   } 
  9. var p = new Point(3, 4); 
  10. console.log(p.toString()); //'(3,4)'  

更多有关于 ES6 中类的使用请参考阮一峰老师的 Class基本语法。

其实 ES6 中的 class 只是写对象原型的时候更方便,更像面向对象,class 的功能 ES5 完全可以做到,比如就上面的例子:


 
 
  1. typeof Point; //'function' 
  2. Point.prototype; 
  3. /* 
  4. |Object 
  5. |--> constructor: function (x, y) 
  6. |--> toString: function() 
  7. |--> __proto__: Object 
  8. */  

和用 ES5 实现的真的没有什么差别,反而现在流行的一些库比 ES6 的 class 能带来更好的效益。

回到最开始的组件问题

那么,说了这么多面向对象,现在回到最开始的那个组件的实现——如何用面向对象来实现。

还是利用 John Resig 构造 class 的方法:


 
 
  1. var JudgeInput = Class.extend({ 
  2.   init: function(config){ 
  3.     this.input = document.getElementById(config.id); 
  4.     this._bind(); 
  5.   }, 
  6.   _getValue: function(){ 
  7.     return this.input.value; 
  8.   }, 
  9.   _render: function(){ 
  10.     var value = this._getValue(); 
  11.     if(!document.getElementById("show")){ 
  12.       var append = document.createElement('span'); 
  13.       append.setAttribute("id""show"); 
  14.       input.parentNode.appendChild(append); 
  15.     } 
  16.     var show = document.getElementById("show"); 
  17.     if(/^[0-9a-zA-Z]+$/.exec(value)){ 
  18.       show.innerHTML = 'Pass!'
  19.     }else
  20.       show.innerHTML = 'Failed!'
  21.     } 
  22.   }, 
  23.   _bind: function(){ 
  24.     var self = this; 
  25.     self.input.addEventListener('keyup'function(){ 
  26.       self._render(); 
  27.     }); 
  28.   } 
  29. }); 
  30. window.onload = function(){ 
  31.   new JudgeInput({id: "input"}); 
  32. }  

但是,这样子,基本功能算是实现了,关键是不好扩展,没有面向对象的精髓。所以,针对目前的情况,我们准备建立一个 Base 基类,init 表示初始化,render 函数表示渲染,bind 函数表示绑定,destory 用来销毁,同时 get、set 方法提供获得和更改属性:


 
 
  1. var Base = Class.extend({ 
  2.   init: function(config){ 
  3.     this._config = config; 
  4.     this.bind(); 
  5.   }, 
  6.   get: function(key){ 
  7.     return this._config[key]; 
  8.   }, 
  9.   setfunction(key, value){ 
  10.     this._config[key] = value; 
  11.   }, 
  12.   bind: function(){ 
  13.     //以后构造 
  14.   }, 
  15.   render: function(){ 
  16.     //以后构造 
  17.   }, 
  18.   destory: function(){ 
  19.     //定义销毁方法 
  20.   } 
  21. });  

基于这个 Base,我们修改 JudgeInput 如下:


 
 
  1. var JudgeInput = Base.extend({ 
  2.   _getValue: function(){ 
  3.     return this.get('input').value; 
  4.   }, 
  5.   bind: function(){ 
  6.     var self = this; 
  7.     self.get('input').addEventListener('keyup'function(){ 
  8.       self.render(); 
  9.     }); 
  10.   }, 
  11.   render: function(){ 
  12.     var value = this._getValue(); 
  13.     if(!document.getElementById("show")){ 
  14.       var append = document.createElement('span'); 
  15.       append.setAttribute("id""show"); 
  16.       input.parentNode.appendChild(append); 
  17.     } 
  18.     var show = document.getElementById("show"); 
  19.     if(/^[0-9a-zA-Z]+$/.exec(value)){ 
  20.       show.innerHTML = 'Pass!'
  21.     }else
  22.       show.innerHTML = 'Failed!'
  23.     } 
  24.   } 
  25. }); 
  26. window.onload = function(){ 
  27.   new JudgeInput({input: document.getElementById("input")}); 
  28. }  

比如,我们后期修改了判断条件,只有当长度为 5-10 的时候才会返回 success,这个时候能很快定位到 JudgeInput 的 render 函数:


 
 
  1. render: function(){ 
  2.   var value = this._getValue(); 
  3.   if(!document.getElementById("show")){ 
  4.     var append = document.createElement('span'); 
  5.     append.setAttribute("id""show"); 
  6.     input.parentNode.appendChild(append); 
  7.   } 
  8.   var show = document.getElementById("show"); 
  9.   //修改正则即可 
  10.   if(/^[0-9a-zA-Z]{5,10}$/.exec(value)){ 
  11.     show.innerHTML = 'Pass!'
  12.   }else
  13.     show.innerHTML = 'Failed!'
  14.   } 
  15. }  

以我目前的能力,只能理解到这里了。

总结

从一个组件出发,一步一步爬坑,又跑去介绍 JS 中的面向对象,如果你能看到最后,那么你就可动手一步一步实现一个 JQuery 了,纯调侃。

关于一个组件的写法,从入门级到最终版本,一波三折,不仅要考虑代码的实用性,还要兼顾后期维护。JS 中实现面向对象,刚接触 JS 的时候,我能用简单的原型链来实现,后来看了一些文章,发现了不少问题,在看 John Resig 的 Class,感触颇深。还好,现在目的是实现了,共勉!


作者:songjz

来源:51CTO

相关文章
|
21天前
|
JavaScript 前端开发
如何在 JavaScript 中使用 __proto__ 实现对象的继承?
使用`__proto__`实现对象继承时需要注意原型链的完整性和属性方法的正确继承,避免出现意外的行为和错误。同时,在现代JavaScript中,也可以使用`class`和`extends`关键字来实现更简洁和直观的继承语法,但理解基于`__proto__`的继承方式对于深入理解JavaScript的面向对象编程和原型链机制仍然具有重要意义。
|
4月前
|
JavaScript 前端开发 开发者
哇塞!Vue.js 与 Web Components 携手,掀起前端组件复用风暴,震撼你的开发世界!
【8月更文挑战第30天】这段内容介绍了Vue.js和Web Components在前端开发中的优势及二者结合的可能性。Vue.js提供高效简洁的组件化开发,单个组件包含模板、脚本和样式,方便构建复杂用户界面。Web Components作为新兴技术标准,利用自定义元素、Shadow DOM等技术创建封装性强的自定义HTML元素,实现跨框架复用。结合二者,不仅增强了Web Components的逻辑和交互功能,还实现了Vue.js组件在不同框架中的复用,提高了开发效率和可维护性。未来前端开发中,这种结合将大有可为。
161 0
|
1月前
|
JavaScript 前端开发
Javascript如何实现继承?
【10月更文挑战第24天】JavaScript 中实现继承的方式有很多种,每种方式都有其优缺点和适用场景。在实际开发中,我们需要根据具体的需求和情况选择合适的继承方式,以实现代码的复用和扩展。
|
24天前
|
JavaScript 前端开发
如何使用原型链继承实现 JavaScript 继承?
【10月更文挑战第22天】使用原型链继承可以实现JavaScript中的继承关系,但需要注意其共享性、查找效率以及参数传递等问题,根据具体的应用场景合理地选择和使用继承方式,以满足代码的复用性和可维护性要求。
|
24天前
|
JavaScript 前端开发 开发者
js实现继承怎么实现
【10月更文挑战第26天】每种方式都有其优缺点和适用场景,开发者可以根据具体的需求和项目情况选择合适的继承方式来实现代码的复用和扩展。
31 1
|
3月前
|
自然语言处理 JavaScript 前端开发
一文梳理JavaScript中常见的七大继承方案
该文章系统地概述了JavaScript中七种常见的继承模式,包括原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承等,并探讨了每种模式的实现方式及其优缺点。
一文梳理JavaScript中常见的七大继承方案
|
2月前
|
JavaScript 前端开发 API
探索Vue.js 3的组合式API:一种更灵活的组件状态管理方式
【10月更文挑战第5天】探索Vue.js 3的组合式API:一种更灵活的组件状态管理方式
|
3月前
|
JavaScript 前端开发
js之class继承|27
js之class继承|27
|
3月前
|
JSON JavaScript 前端开发
js原型继承|26
js原型继承|26
|
3月前
|
JavaScript 前端开发 开发者
JavaScript 类继承
JavaScript 类继承
22 1