【一次面試】再談javascript中的繼承


前言

面向對象編程是每次面試必問的知識點,而前端js如何實現繼承每次命中率高達80%

這不,近兩天我們面試時候,同事就問道面試者此問題,但是,不論之前自己做的回答,還是面試者的回答,基本都不太令人滿意

很大的原因是多數時候前端並不需要實現繼承,就jquery來說也基本上是一碼到底,沒有實現繼承,據我所知,也就prototype與ext實現過繼承

所以繼承對前端來說似乎不太適用

近兩年來情況有所變化,SPA的興起以及前端邏輯的復雜化,讓前端代碼愈發的多,愈發的重,所以繼承慢慢的進入了一些初級一點的前端視野

所以,好好的了解如何實現繼承,繼承的幾個用法,是非常有意義的,就算只是為面試都是很有用的

文章只是個人見解,有誤請提出,demo未做檢測,有誤請提出

實現繼承

當一個函數被創建時,Function構造函數產生的函數會隱式的被賦予一個prototype屬性,prototype包含一個constructor對象

而constructor便是該新函數對象(constructor意義不大,但是可以幫我們找到繼承關系)

每個函數都會有一個prototype屬性,該屬性指向另一對象,這個對象包含可以由特定類型的所有實例共享的屬性和方法

每次實例化后,實例內部都會包含一個[[prototype]](__proto__)的內部屬性,這個屬性指向prototype

① 我們通過isPrototypeOf來確定某個對象是不是我的原型
② hasOwnPrototype 可以檢測一個屬性是存在實例中還是原型中,該屬性不是原型屬性才返回true
var Person = function (name, age) {
    this.name = name;
    this.age = age;
};
Person.prototype.getName = function () {
    return this.name;
};
var y = new Person('葉小釵', 30);

通俗一點來說,prototype是一模板,新創建對象就是對他一個拷貝,里面的屬性或者方法都會賦值給實例

這里說是模板賦值其實不太合理,反正由類產生的所有實例的__proto__都會共享一個prototype,這里我做一個例子

我們在斷點情況下是沒有name2屬性的,但是我們如果在斷點下加上這個代碼的話,a.name2,就有值了

Klass.prototype.name2 = '222';

所以,這里說模板,不如說是指針指向,都是共享一個對象;繼承的情況的話就是這樣

(function () {
    var Person = function (name) {
        this.name = name;
    };
    //Person.prototype = {};//這句將影響十分具有constructor屬性
    Person.prototype.getName = function () {
        return this.name;
    };

    var Student = function (name, sex, id) {
        this.name = name || '無名氏';
        this.sex = sex || '不明';
        this.id = id || '未填'; //學號
    };
    //相當於將其prototype復制了一次,若是包含constructor的話將指向Person
    Student.prototype = new Person();
    Student.prototype.getId = function () {
        return this.id;
    }
    var y = new Person();
    var s = new Student;
    var s1 = y instanceof Person;
    var s2 = s instanceof Student;
    var s3 = s instanceof Person;
    var s4 = Student.prototype.constructor === Person;
    var s5 = Student.constructor === Person;
    var s6 = Student.constructor === Function;

    var s = '';
})();
View Code

prototype實現繼承

我們在具體項目中,真正復雜一點的項目可能就會對繼承進行封裝,讓自己更好的使用,我們下面就來看看prototype怎么干的

PS:我這里做了一點小的修改:

  1 var Class = (function () {
  2   function subclass() { };
  3 
  4   //我們構建一個類可以傳兩個參數,第一個為需要繼承的類,
  5   //如果沒有的話就一定會有第二個對象,就是其原型屬性以及方法,其中initialize為構造函數的入口
  6   function create() {
  7 
  8     //此處兩個屬性一個是被繼承的類,一個為原型方法
  9     var parent = null;
 10     var properties = $A(arguments);
 11 
 12     if (Object.isFunction(properties[0]))
 13       parent = properties.shift();
 14 
 15     //新建類,這個類最好會被返回,構造函數入口為initialize原型方法
 16     function klass() {
 17       this.initialize.apply(this, arguments);
 18     }
 19 
 20     //擴展klass類的“實例”對象(非原型),為其增加addMethods方法
 21     Object.extend(klass, Class.Methods);
 22 
 23     //為其指定父類,沒有就為空
 24     klass.superclass = parent;
 25 
 26     //其子類集合(require情況下不一定准確)
 27     klass.subclasses = [];
 28 
 29     //如果存在父類就需要繼承
 30     if (parent) {
 31       //新建一個空類用以繼承,其存在的意義是不希望構造函數被執行
 32       //比如 klass.prototype = new parent;就會執行其initialize方法
 33       subclass.prototype = parent.prototype;
 34       klass.prototype = new subclass;
 35       parent.subclasses.push(klass);
 36     }
 37 
 38     //遍歷對象(其實此處這樣做意義不大,我們可以強制最多給兩個參數)
 39     //注意,此處為一個難點,需要謹慎,進入addMethods
 40     for (var i = 0, length = properties.length; i < length; i++)
 41       klass.addMethods(properties[i]);
 42 
 43     if (!klass.prototype.initialize)
 44       klass.prototype.initialize = Prototype.emptyFunction;
 45 
 46     klass.prototype.constructor = klass;
 47     return klass;
 48   }
 49 
 50   /**
 51   由於作者考慮情況比較全面會想到這種情況
 52   var Klass = Class.create(parent,{},{});
 53   后面每一個對象的遍歷都會執行這里的方法,我們平時需要將這里直接限定最多兩個參數
 54   */
 55   function addMethods(source) {
 56 
 57     //當前類的父類原型鏈,前面被記錄下來了
 58     var ancestor = this.superclass && this.superclass.prototype;
 59 
 60     //將當前對象的鍵值取出轉換為數組
 61     var properties = Object.keys(source);
 62 
 63     //依次遍歷各個屬性,填充當前類(klass)原型鏈
 64     for (var i = 0, length = properties.length; i < length; i++) {
 65 
 66       //property為鍵,value為值,比如getName: function(){}的關系
 67       var property = properties[i], value = source[property];
 68 
 69       /****************
 70       這里有個難點,用於處理子類中具有和父類原型鏈同名的情況,仍然可以調用父類函數的方案(我這里只能說牛B)
 71       如果一個子類有一個參數叫做$super的話,這里就可以處理了,這里判斷一個函數的參數使用了正則表達式,正如
 72       var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
 73       ****************/
 74       if (ancestor && Object.isFunction(value) && value.argumentNames()[0] == "$super") {
 75         
 76         //將當前函數存下來
 77         var method = value;
 78         /****************
 79         第一步:
 80 
 81         這里是這段代碼最難的地方,需要好好閱讀,我們首先將里面一塊單獨提出
 82         value = (function (m) {
 83         return function () { return ancestor[m].apply(this, arguments); };
 84         })(property)
 85         這里很牛B的構建了一個閉包(將方法名傳了進去),任何直接由其父類原型中取出了相關方法
 86         然后內部返回了該函數,此時其實重寫了value,value
 87         ***這里***有一個特別需要注意的地方是,此處的apply方法不是固定寫在class上的,是根據調用環境變化的,具體各位自己去理解了
 88         
 89         第二步:
 90         首先value被重新成其父類的調用了,此處可以簡單理解為(僅僅為理解)$super=value
 91         然后下面會調用wrap操作vaule將,我們本次方法進行操作
 92         wrap: function (wrapper) {
 93           var __method = this;
 94           return function () {
 95             return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
 96           }
 97         }
 98         可以看出,他其實就是將第一個方法(value)作為了自己方法名的第一個參數了,后面的參數不必理會
 99         ****************/
100         value = (function (m) {
101           return function () { return ancestor[m].apply(this, arguments); };
102         })(property).wrap(method);
103       }
104       //為其原型賦值
105       this.prototype[property] = value;
106     }
107     return this;
108   }
109 
110   return {
111     create: create,
112     Methods: {
113       addMethods: addMethods
114     }
115   };
116 })();

下面來一個簡單的例子:

 1 var Person = Class.create({
 2   initialize: function (name) {
 3     this.name = name;
 4   },
 5   getName: function () {
 6     console.log('我是父類');
 7     return  this.name;
 8   },
 9   getAge: function () {
10     return this.age;
11   }
12 });
13 
14 var Employee = Class.create(Person, {
15   initialize: function ($super, name, age) {
16     $super(name);
17     this.age = age;
18   },
19   getName: function ($super) {
20     return '我是子類:' + $super();
21   }
22 });
23 
24 var C = Class.create(Employee, {
25   getAge: function ($super) {
26     return $super();
27   }
28 });
29 
30 var y = new C("葉小釵", 25);
31 console.log(y.getName() + ': ' + y.getAge());
View Code

這里,我們根據自己的需求重寫寫了下繼承相關代碼,表現基本與上述一致,各位可以自己試試

PS:當然如果有問題請指出

簡化prototype繼承

 1 var arr = [];
 2 var slice = arr.slice;
 3 
 4 function create() {
 5   if (arguments.length == 0 || arguments.length > 2) throw '參數錯誤';
 6 
 7   var parent = null;
 8   //將參數轉換為數組
 9   var properties = slice.call(arguments);
10 
11   //如果第一個參數為類(function),那么就將之取出
12   if (typeof properties[0] === 'function')
13     parent = properties.shift();
14   properties = properties[0];
15 
16   function klass() {
17     this.initialize.apply(this, arguments);
18   }
19 
20   klass.superclass = parent;
21   klass.subclasses = [];
22 
23   if (parent) {
24     var subclass = function () { };
25     subclass.prototype = parent.prototype;
26     klass.prototype = new subclass;
27     parent.subclasses.push(klass);
28   }
29 
30   var ancestor = klass.superclass && klass.superclass.prototype;
31   for (var k in properties) {
32     var value = properties[k];
33 
34     //滿足條件就重寫
35     if (ancestor && typeof value == 'function') {
36       var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
37       //只有在第一個參數為$super情況下才需要處理(是否具有重復方法需要用戶自己決定)
38       if (argslist[0] === '$super' && ancestor[k]) {
39         value = (function (methodName, fn) {
40           return function () {
41             var scope = this;
42             var args = [function () {
43               return ancestor[methodName].apply(scope, arguments);
44             } ];
45             return fn.apply(this, args.concat(slice.call(arguments)));
46           };
47         })(k, value);
48       }
49     }
50 
51     klass.prototype[k] = value;
52   }
53 
54   if (!klass.prototype.initialize)
55     klass.prototype.initialize = function () { };
56 
57   klass.prototype.constructor = klass;
58 
59   return klass;
60 }

如此,我們就完成了自己的繼承了

實戰繼承

知道原型可以實現繼承是皮毛,知道各個庫是怎樣干的也只是入門
因為,要在項目中用到才能算真正掌握繼承,這里我們便來點實戰的小例子
這里我寫一個簡單的view用於下面各種繼承

 1 var AbstractView = create({
 2   initialize: function (opts) {
 3     opts = opts || {};
 4     this.wrapper = opts.wrapper || $('body');
 5 
 6     //事件集合
 7     this.events = {};
 8 
 9     this.isCreate = false;
10 
11   },
12   on: function (type, fn) {
13     if (!this.events[type]) this.events[type] = [];
14     this.events[type].push(fn);
15   },
16   trigger: function (type) {
17     if (!this.events[type]) return;
18     for (var i = 0, len = this.events[type].length; i < len; i++) {
19       this.events[type][i].call(this)
20     }
21   },
22   createHtml: function () {
23     throw '必須重寫';
24   },
25   create: function () {
26     this.root = $(this.createHtml());
27     this.wrapper.append(this.root);
28     this.trigger('onCreate');
29     this.isCreate = true;
30   },
31   show: function () {
32     if (!this.isCreate) this.create();
33     this.root.show();
34     this.trigger('onShow');
35   },
36   hide: function () {
37     this.root.hide();
38   }
39 });
40 
41 var Alert = create(AbstractView, {
42 
43   createHtml: function () {
44     return '<div class="alert">這里是alert框</div>';
45   }
46 });
47 
48 var AlertTitle = create(Alert, {
49   initialize: function ($super) {
50     this.title = '';
51     $super();
52 
53   },
54   createHtml: function () {
55     return '<div class="alert"><h2>' + this.title + '</h2>這里是帶標題alert框</div>';
56   },
57 
58   setTitle: function (title) {
59     this.title = title;
60     this.root.find('h2').html(title)
61   }
62 
63 });
64 
65 var AlertTitleButton = create(AlertTitle, {
66   initialize: function ($super) {
67     this.title = '';
68     $super();
69 
70     this.on('onShow', function () {
71       var bt = $('<input type="button" value="點擊我" />');
72       bt.click($.proxy(function () {
73         alert(this.title);
74       }, this));
75       this.root.append(bt)
76     });
77   }
78 });
79 
80 var v1 = new Alert();
81 v1.show();
82 
83 var v2 = new AlertTitle();
84 v2.show();
85 v2.setTitle('我是標題');
86 
87 var v3 = new AlertTitleButton();
88 v3.show();
89 v3.setTitle('我是標題和按鈕的alert');
View Code

http://sandbox.runjs.cn/show/ddlp7nlt

結語

希望這次繼承的文章對各位有幫助,此外文中錯誤請指出

親愛的道友們,我其實在我們團隊只是中等水平,我們上海攜程無線是一個優秀的團隊,
如果你現在正在找工作,請加入我們吧!!!
在我們團隊,你可以肆無忌憚的黑自己的老大,你會體會到和諧的氛圍,當然妹子多多的!
 
要求:靠譜前端,吃苦耐勞,待遇剛剛的!!!

需要的朋友可以私信我

順便推廣道友的jquery技術交流群:239147101

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM