從一個組件的實現來深入理解 JS 中的繼承

其實,不管是寫什麼語言的程序員,最終的目的,都是把產品或代碼封裝到一塊兒,提供接口,讓使用者很溫馨的實現功能。因此對於我來講,每每頭疼的不是寫代碼,而是寫註釋和文檔!若是接口很亂,確定會頭疼一成天。javascript

clipboard.png

JavaScript 最初是以 Web 腳本語言面向大衆的,儘管如今出了服務器端的 nodejs,可是單線程的性質尚未變。對於一個 Web 開發人員來講,能寫一手漂亮的組件極爲重要。GitHub 上那些開源且 stars 過百的 Web 項目或組件,可讀性確定很是好。html

從一個例子來學習寫組件

組件教程的參考來自於 GitHub 上,通俗易懂,連接前端

要實現下面這個功能,對一個 input 輸入框的內容進行驗證,只有純數字和字母的組合纔是被接受的,其餘都返回 failed:java

clipboard.png

全局變量寫法

這種寫法徹底沒有約束,基本全部人都會,徹底沒啥技巧: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__ 有這個屬性,這就是鏈式調用的原理,一層一層向下尋找。

這就是最簡單的繼承了。

面向對象的進階

來看一看剛纔那種作法的弊端。

  1. 沒有實現傳統面向對象該有的 super 方法來調用父類方法,鏈式和 super 方法相比仍是有必定缺陷的;

  2. 形成過多的原型屬性(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 方法。

clipboard.png

再找緣由,這句話致使了 Student.prototype 的 constructor 方法丟失:

Student.prototype = new Person();

在這句話以前打一個斷點,曾經是有的,只是被替換掉了:

clipboard.png

找到了問題所在,如今來改進:

// 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 的頭腦真是使人佩服。

ES6 中的 class

在 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 用來銷燬,同時 getset 方法提供得到和更改屬性:

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。

相關文章
相關標籤/搜索