【2019 前端進階之路】JavaScript 原型和原型鏈及 canvas 驗證碼實踐

江三瘋發表於2019-03-03

前言

最近在複習 JavaScript 基礎,因為工作以後基本上沒用過,天天都是拿起框架加油幹,確實大部分都忘了。到了原型和原型鏈這一部分,覺得自己理解的比較模糊。又翻閱了《你不知道的 JavaScript》、阮一峰老師的JavaScript繼承機制的設計思想還有網路上的各種文章,收穫滿滿(感謝各位作者大佬)。所以整理成這篇文章,加深自己的印象,也希望對大家有所幫助。

文章收錄在作者程式碼庫 fe-code,主要是個人學習的程式碼以及文章,覺得有幫助可以點個小星星,會持續更新。

另外也希望大家可以支援一下我的開源作品 Vchat — 從頭到腳,擼一個社交聊天系統(vue + node + mongodb),這是 原始碼倉庫。感謝!

預告

本文主要分兩個部分,第一部分講原理(原型和原型鏈),第二部分則是實踐(封裝 Canvas 驗證碼、手寫 Promise),實際應用原型、建構函式,做到學以致用。 如果你對原型和原型鏈已經很熟悉了,也可以直接跳過原理部分,直接看實踐

思維導圖

【2019 前端進階之路】JavaScript 原型和原型鏈及 canvas 驗證碼實踐

不太瞭解原型鏈的同學可能會覺得有點亂,沒關係,看完文章再回過頭來看,就很清晰了。

原理

打好基礎,才能建設萬丈高樓。

Prototype

眾所周知,在 JavaScript 中,可以通過關鍵字 new 呼叫建構函式來建立一個例項物件。

    function Person(name){
        this.name = name;
        this.say = function () {
            console.log(this.name);
        }
    }
    let lisi = new Person('lisi');
    let liwu = new Person('liwu');
    lisi.say() // lisi
    liwu.say() // liwu
    console.log(lisi.say === liwu.say); // false
複製程式碼

可以看出, lisi 和 liwu 都有 say 這個方法,但是這兩個方法並不是同一個。也就是說在建立物件的時候,每個例項物件都會有一套自己的屬性和方法。很顯然,這樣造成了資源浪費。

這時候我們想,如果可以讓例項物件引用同一個屬性或方法就好了。所以 JavaScript 的作者引入了原型物件 [Prototype] 來解決這個問題。原型物件上有兩個預設屬性, constructor 和 __proto__ (下文會詳細講)。

    function Person(name){
        this.name = name;
    }
    Person.prototype.say = function () {
        console.log(this.name);
    }
    let lisi = new Person('lisi');
    let liwu = new Person('liwu');
    console.log(lisi.say === liwu.say); // true
    console.log(lisi.hasOwnProperty('say'), liwu.hasOwnProperty('say')); // false false
複製程式碼

這個時候可以看到,構造的新的例項物件都有 say 方法,但是hasOwnProperty('say')返回的結果卻是 false 。這說明例項物件自身是沒有 say 方法的,之所以可以使用 .say 的方式來呼叫,是因為在使用 . 語法呼叫物件方法的時候會觸發物件自身的 [get] 操作。

[get] 操作會優先查詢自身的屬性,沒有找到則會通過原型鏈來逐級查詢上級的原型物件,直到 JavaScript 頂層的 Object 物件。所以此處可以說明例項物件會繼承建構函式的原型物件上的屬性和方法。

但是正因為如此,我們需要注意的是:因為原型物件的屬性和方法是會被所有例項物件繼承的,所以使用的時候要慎重考慮該屬性或方法是否適合放在原型物件上。比如Person有一個 age 屬性:

    Person.prototype.age = 18;
    console.log(lisi.age, liwu.age); // 18 18
    Person.prototype.age = 20;
    console.log(lisi.age, liwu.age); // 20 20
複製程式碼

因為 age 屬性是引用的Person的原型物件上的,所以原型物件上的屬性值改了,所有的例項物件相應的屬性值都會改動。這時候我們就不得不考慮,是否有必要將 age 屬性放在原型物件了,畢竟魯迅曾經說過:‘每個人都是都一無二的’。

【2019 前端進階之路】JavaScript 原型和原型鏈及 canvas 驗證碼實踐

強行插圖,哈哈哈!我們再來看下面這種情況:

    lisi.say = function() {
        console.log('oh nanana');
    };
    lisi.say(); // oh nanana
    liwu.say(); // liwu
    console.log(lisi.hasOwnProperty('say'), liwu.hasOwnProperty('say')); // true false
複製程式碼

這是為什麼呢,其實和之前類似,是因為.語法在賦值的時候觸發了物件的 [set] 方法,所以會給 lisi 自身加上一個 say 方法。而在呼叫方法時,最先找到自身的 say 方法呼叫,輸出oh nanana 。因為操作都是在 lisi 這個物件本身,所以對 liwu 沒有影響。

constructor

constructor 即為 建構函式,建構函式其實和普通的函式沒有什麼區別,對建構函式使用new運算子,就能生成例項,並且 this 變數會繫結在例項物件上。

對於Person來講,會有 prototype 屬性指向它的原型物件,而在 Person.prototype 上又有 constructor 屬性指向它對應的建構函式,所以這是一個迴圈的引用。大概是這樣:Person -> Person.prototype —> Person.prototype.constructor -> Person

    console.log(Person.constructor === Function) // true
    console.log(Person.prototype.constructor === Person) // true
    console.log(lisi.constructor === Person.prototype.constructor); // true
    console.log(lisi.hasOwnProperty('constructor')) // false
複製程式碼

從中可以看出,Person的 constructor 是 Function,lisi 的 constructor 是 Person。這是因為,他們自身是都沒有 constructor 屬性的,而是從他們所繼承的原型物件上繼承得來的 constructor 屬性。即 lisi.constructor === Person.prototype.constructorPerson.constructor === Function.prototype.constructor

延用上面的栗子,我們在加點東西:

    function Chinese() {
        this.country = '中國';
    }
    Person.prototype = new Chinese();
    let lisisi = new Person('lisisi');
    console.log(lisi.country, lisisi.country); // undefined  中國
複製程式碼

在這個栗子中,我們將Person.prototype整體賦值成了Chinese的例項物件。注意,是賦值的例項物件,不是建構函式。上面列印結果是 lisisi 有 country 屬性,這個我們好理解,因為 lisisi 繼承了Person.prototype ,而Person.prototype被我們賦值成了Chinese的例項物件,自然會繼承Chinese例項物件的 country 屬性。

但是 lisi 為什麼沒有 country 屬性呢,之前改得 say 方法明明受影響啊。我們列印出 lisi 和 lisisi 的完整結構來看一下:

【2019 前端進階之路】JavaScript 原型和原型鏈及 canvas 驗證碼實踐

可以看到,其實是因為我們將Person.prototype整體替換成了 Chinese 例項物件,相當於改變了Person.prototype的地址,但是 lisi 在例項化的時候,引用的是之前的Person.prototype地址,這兩者之間沒有聯絡,自然不會有影響。而之前的 say 方法是用Person.prototype.say的形式改的,lisi 繼承的依舊是同一地址上的 say 方法,所以會受影響。

這個例子之所以放在這裡講,而不是 prototype 那裡,是因為這個方法會有一點副作用,將Person.prototype整體賦值成了Chinese的例項物件,會導致原來的 constructor 屬性也被覆蓋掉。

    console.log(lisisi instanceof Person); // true
    console.log(Person.prototype.isPrototypeOf(lisisi)); // true
    console.log(Object.getPrototypeOf(lisisi)); // Chinese {country: "中國"}
    
    // instanceof做的事是判斷在`lisisi`的整條[Prototype]鏈中是否有指向 Person.prototype 的物件。
    // isPrototypeOf做的事是判斷在`lisisi`的整條[Prototype]鏈中是否出現過 Person.prototype。
    // 它們的區別在於前者要訪問建構函式,後者直接訪問原型物件。
    
    console.log(lisisi.__proto__ === Person.prototype); // true   
    // __proto__指向例項物件對應的原型物件,但不一定是其建構函式的原型物件,因為prototype可以修改
    
    console.log(lisisi.constructor === Chinese); // true
    
複製程式碼

從上可以看出,雖然 lisisi 繼承的依然是的Person.prototype,但是由於Person.prototype指向了Chinese的例項物件。所以,這個時候 lisisi 的 constructor 已經不是Person了,而是繼承了Chinese例項物件的 constructor,也就是建構函式Chinese。為了解決這個問題,我們需要手動修正 constructor 的指向。

    Person.prototype = new Chinese();
    Person.prototype.constructor = Person;
    let lisisi = new Person('lisisi');
    console.log(lisisi.constructor === Person); // true
複製程式碼

從這個栗子也可以說明,使用引用型別的 constructor 是並不安全的,因為他們可以修改。不過基礎型別的 constructor 都是隻讀的,都指向對應基礎型別建構函式。

    let a = 'oh nanana', b = 0, c = true;
    console.log(a.constructor, b.constructor, c.constructor);
    // ƒ String() { [native code] } ƒ Number() { [native code] } ƒ Boolean() { [native code] }
    
    a.constructor = {};
    b.constructor = {}; 
    c.constructor = {};
    console.log(a.constructor, b.constructor, c.constructor);
    // ƒ String() { [native code] } ƒ Number() { [native code] } ƒ Boolean() { [native code] }
複製程式碼

__proto__

例項物件有__proto__屬性,指向例項物件對應的原型物件,即lisi.__proto__ === Person.prototype。但是直接用.__proto__的寫法來設定原型物件的寫法是不被贊同的,因為這樣還會有除了效能消耗以外的問題。MDN 中這樣說到:

由於現代 JavaScript 引擎優化屬性訪問所帶來的特性的關係,更改物件的 [[Prototype]]在各個瀏覽器和 JavaScript 引擎上都是一個很慢的操作。其在更改繼承的效能上的影響是微妙而又廣泛的,這不僅僅限於 obj.proto = ... 語句上的時間花費,而且可能會延伸到任何程式碼,那些可以訪問任何[[Prototype]]已被更改的物件的程式碼。如果你關心效能,你應該避免設定一個物件的 [[Prototype]]。相反,你應該使用 Object.create()來建立帶有你想要的[[Prototype]]的新物件。

在《你不知道的JavaScript》中說到,__proto__的本質其實更像是getter/setter,大致實現為:

    Object.defineProperty( Object.prototype, "__proto__", {
        get: function() {
            return Object.getPrototypeOf( this );
        },
        set: function(o) {
            // ES6 中的 setPrototypeOf(obj, prototype) 設定原型物件
            Object.setPrototypeOf(this, o );
            return o;
        }
    } );
複製程式碼

何為原型鏈

現在我們知道,例項物件的__proto__屬性指向其對應的原型物件。而在原型物件 prototype 上又有 constructor 和__proto__屬性,此時的__proto__又指向上級對應的原型物件,最終指向Object.prototype, 而Object.prototype.__proto__ === null。這就構成了原型鏈,而原型鏈最終都是指向 null。

還是來看個栗子:

    function Person(name){
        this.name = name;
    }
    let lisi = new Person('lisi');
複製程式碼

在這個栗子中可以找到兩條原型鏈,我們逐一來看。

  • 第一條:首先,lisi.__proto__ === Person.prototype,而原型物件也是物件,所以 Person.prototype.__proto__ === Object.prototype,最後,Object.prototype.__proto__ === null。即:
    lisi.__proto__.__proto__.__proto__ === null;
複製程式碼
  • 第二條:Person這個函式物件的__proto__指向的應該是它的建構函式對應的原型物件,Person.__proto__ === Funcion.prototype,然後Funcion.prototype.__proto__ === Object.prototype,最後一樣都回到 null。即:
    Person.__proto__.__proto__.__proto__ === null;
複製程式碼

到這裡,相信你已經可以理解文章開頭的那張圖了。

new方法做了什麼

文章中建立例項物件是通過new運算子。new命令的作用,就是執行建構函式,返回一個例項物件。

那麼在執行new操作的過程中到底做了哪些事呢?我們可以看到,new 操作返回的例項物件具有兩個特徵:

  1. 具有建構函式中定義的 this 指標的屬性和方法
  2. 具有建構函式原型上的屬性和方法

於是我們大概可以知道,使用new命令時它所執行的幾個步驟:

  1. 建立一個空物件,並將這個空物件的__proto__,指向建構函式的原型物件 [prototype] ,使其繼承建構函式原型上的屬性。
  2. 改變建構函式內部 this 指標為這個空物件(如果有傳參,需要將引數也匯入建構函式)
  3. 執行建構函式中的程式碼,使其具有建構函式 this 指標的屬性。

所以我們可以簡單模擬實現一個具有new命令功能的函式。

    function newObj() {
        let o, f = [].shift.call(arguments); // 取出引數的第一個成員,即建構函式
        o = Object.create(f.prototype); // 建立一個繼承了建構函式原型的新物件
        f.call(o, ...arguments); // 執行建構函式使得新物件獲取相應屬性
        return o;
    }
    let zs = newObj(Person, 'zs');
    console.log(zs instanceof Person); // true
複製程式碼

我們列印一下 zs 例項物件:

【2019 前端進階之路】JavaScript 原型和原型鏈及 canvas 驗證碼實踐

可以看出 zs 是繼承了Person的原型的,但是還有一個需要注意的點:假如建構函式 return 了一個物件的話,new命令會優先返回建構函式 return 的物件。如果是其他型別的資料,則會忽略,和沒有返回值(函式預設返回 undefined )是一樣的。這裡就不再舉例,感興趣的夥伴可以自己實踐一下,也有助於理解。

實踐 Canvas驗證碼

光說不練假把式,實踐過程也能幫助我們更好地理解。

以下內容需要一些基礎的 Canvas 知識,不太瞭解的同學建議結合 Canvas 參考手冊 一起看,本文重點講實現流程。原始碼依然在 fe-code

為什麼選擇驗證碼來做這個實踐呢,因為這在我們平時的專案非常常見。也許由於需求等各種原因我們平時用的是外掛或者是後端返回的驗證碼,但是沒關係,我們可以藉此作為練習,加深對建構函式和原型的理解。

需求

首先,我們要實現這樣一個圖片驗證碼。

【2019 前端進階之路】JavaScript 原型和原型鏈及 canvas 驗證碼實踐

簡單分析一下幾點需求:

  1. 隨機四個(或n個)數字字母(或漢字或其他),隨機顏色,隨機排列。
  2. 數個點隨機顏色,隨機排列;數條線隨機顏色,隨機長度,隨機排列。
  3. 隨機背景色。
  4. 點選更新檢視。
  5. 最重要的一點是需要可以拿到每次圖片上的文字,進而與使用者輸入驗證碼比對。

實現

瞭解了上面的幾點需求,回想一下之前學習的內容,再來思考一下如何實現。

現在我們需要一個物件,然後呼叫物件的某個方法可以將驗證碼畫出來。所以我們需要一個建構函式,用來例項化物件。

    function Regcode() {}
複製程式碼

建構函式接受一些引數,用來定製驗證碼的點、線、字的各種屬性(顏色、長短、大小等)。

    function Regcode(params = {}) {
        let p = Object.assign({...}, params); // 這裡有定義好的屬性和預設值
        Object.keys(p).forEach(k => { // 將所有屬性組合後新增到this上
            this[k] = p[k];
        });
    }
複製程式碼

draw 方法

可是我們現在並不知道需要哪些引數,但是根據需求我們可以先定下大概的框架。首先我們需要一個 draw 方法,作為驗證碼的繪製方法。draw 方法接收兩個引數,canvas 的 dom 物件,用來建立繪圖的2d物件。還需要一個回撥函式 callback,用來接收每次繪製的文字。

我們把 draw 方法放在Regcode的原型上,這樣所有的例項物件都可以繼承這些方法,而不是自己獨立有一套。

    Regcode.prototype.draw = function(dom, callback = function () {}) { // 繪圖 };
複製程式碼

在 draw 方法中,可以想到的是,我們需要建立 canvas 的 2d物件,建立畫布,然後開始依次繪製點、線、文字。

    Regcode.prototype.draw = function(dom, callback = function () {}) { // 繪圖
        // 獲取canvas dom
        if (!this.paint) { // 如果沒有2d物件,再進行賦值操作
            this.canvas = dom; // 儲存到this指標,方便使用
            if (!this.canvas) return;
            this.paint = this.canvas.getContext('2d'); // 儲存到this指標,方便使用
            if (!this.paint) return;
            
            // 回撥函式賦值給this,方便使用
            this.callback = callback;
        }
        // 隨機畫布顏色,使用背景色
        let colors = this.getColor(this.backgroundColor);
        this.paint.fillStyle = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.8)`;
        // 繪製畫布
        this.paint.fillRect(0, 0, this.canvas.width, this.canvas.height);
        // 繪圖
        this.arc();
        this.line();
        this.font();
    };
複製程式碼

我們需要簡單判斷一下是否有 dom 物件和2d物件,其實應該判斷引數是否為 dom 物件,可以通過判斷節點型別或者通過 dom instanceof HTMLElement(谷歌和火狐支援)來判斷。但是這裡因為要求不高,所以只是簡單判斷。回撥函式只是簡單的賦值給了例項物件,具體的使用稍後再看。

隨機顏色

從中我們可以看到整體的思路,還需要哪些方法。需要注意的是,在建立畫布的時候,我們使用了獲取背景色的一個方法。在之前的需求中我們可以看到,最高頻的兩個詞是隨機和顏色,所以肯定是需要將這兩個方法單獨封裝的。

隨機顏色這裡採用的是 rgb 的強度值(0 ~ 255, 由暗 -> 亮),需要指定兩個顏色區間:前景色(文字、線條)和背景色(畫布背景)。因為需要將文字和背景顏色區分,避免色值太接近無法識別,所以預設前景色區間 [10, 80],背景色區間 [150, 250]。

    Regcode.prototype.getColor = function(arr) { // 隨機獲取顏色
        let colors = new Array(3).fill(''); // 建立一個長度為3的陣列,值都填充為 ''
        colors = colors.map(v => this.getRand(...arr)); // 每個成員隨機獲取一個強度值重組為新陣列
        return colors;
    };
複製程式碼

因為 rgb 顏色通常表示為 rgba(0,0,0,0.8),最後一位是透明度,這裡沒有參加隨機。所以只考慮前3個數,在指定的強度區間內,只需要依次隨機出3個數就好。所以在上面的方法中,還需要做的就是隨機在一個數值區間中取值。

    Regcode.prototype.getRand = function(...arr) { // 獲取某個區間的隨機數
        arr.sort((a, b) => a - b); // 將傳入的引數從小到大排序
        return Math.floor(Math.random() * (arr[1] - arr[0]) + arr[0]);
    };
複製程式碼

繪製線條

有了隨機顏色,繪製線條就方便多了。lineNum 用於指定繪製幾條線,預設為2條。之前說過前景色(foregroundColor) 和 背景色 (backgroundColor)也是可以傳參的,文字、線條、點都使用前景色。在繪製線條的時候,還需要計算出線條的隨機起止座標,在這裡 canvas 的寬高範圍內都允許,這樣就可以做到隨機長度。

    Regcode.prototype.line = function() { // 繪製線條
        for (let i = 0; i < this.lineNum; i++) {
            // 隨機獲取線條的起止座標
            let x = this.getRand(0, this.canvas.width), y = this.getRand(0, this.canvas.height),
                endx = this.getRand(0, this.canvas.width), endy = this.getRand(0, this.canvas.width);
            this.paint.beginPath(); // 開始繪製
            this.paint.lineWidth = this.lineWidth;
            // 隨機獲取路徑顏色
            let colors = this.getColor(this.foregroundColor); // 使用前景色
            this.paint.strokeStyle = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.8)`;
            // 指定繪製路徑
            this.paint.moveTo(x, y);
            this.paint.lineTo(endx, endy);
            this.paint.closePath();
            this.paint.stroke(); // 進行繪製
        }
    };
複製程式碼

繪製圓點

繪製圓點要注意的是需要隨機獲取圓心的位置,即分別隨機獲取在寬高範圍內的 (x, y) 座標。dotNum 是允許傳入的需要繪製圓點的個數,預設為10,dotR 是半徑,預設為 1。

    Regcode.prototype.arc = function() { // 繪製圓點
        for (let i = 0; i < this.dotNum; i++) {
            // 隨機獲取圓心
            let x = this.getRand(0, this.canvas.width), y = this.getRand(0, this.canvas.height);
            this.paint.beginPath();
    
            // 指定圓周路徑
            this.paint.arc(x, y, this.dotR, 0, Math.PI * 2, false);
            this.paint.closePath();
    
            // 隨機獲取路徑顏色
            let colors = this.getColor(this.foregroundColor);
            this.paint.fillStyle = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.8)`;
    
            // 繪製
            this.paint.fill();
        }
    };
複製程式碼

繪製文字

繪製文字稍微麻煩一些,需要先從定義好的驗證碼因子(允許通過 content 引數自定義,預設為 acdefhijkmnpwxyABCDEFGHJKMNPQWXY12345789,這裡去掉了類似於字母 b 和 數字 6 這樣的容易混淆的字元。)中,隨機獲取指定長度(允許通過引數自定義)的驗證碼。

    Regcode.prototype.getText = function() { // 隨機獲取驗證碼
        let len = this.content.length, str = '';
        for (let i = 0; i < this.len; i++) { // 隨機獲取每個因子,組成驗證碼
            str += this.content[this.getRand(0, len)];
        }
        return str;
    };
複製程式碼

繪製文字的時候需要注意以下幾點:

  1. 需要通過回撥函式將當前繪製的文字輸出。
  2. 需要指定文字的旋轉角度、字型型別、文字顏色、繪製風格(填充或者不填充)。
  3. 需要獲得文字的實際寬度,用來確定單個文字的活動範圍。
    Regcode.prototype.font = function() { // 繪製文字
        let str = this.getText(); // 獲取驗證碼
        this.callback(str); // 利用回撥函式輸出文字,用於與使用者輸入驗證碼進行比對
        // 指定文字風格
        this.paint.font = `${this.fontSize}px ${this.fontFamily}`;
        this.paint.textBaseline = 'middle'; // 設定文字基線,middle是整個文字所佔方框的高度的正中。
        // 指定文字繪製風格
        let fontStyle = `${this.fontStyle}Text`;
        let colorStyle = `${this.fontStyle}Style`;
        for (let i = 0; i < this.len; i++) { // 迴圈繪製每個字
            let fw = this.paint.measureText(str[i]).width; // 獲取文字繪製的實際寬度
            // 獲取每個字的允許範圍,用來確定繪製單個文字的橫座標
            let x = this.getRand(this.canvas.width / this.len * i, (this.canvas.width / this.len) * i + fw/2);
            // 隨機獲取字型的旋轉角度
            let deg = this.getRand(-6, 6);
            // 隨機獲取文字顏色
            let colors = this.getColor(this.foregroundColor);
            this.paint[colorStyle] = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.8)`;
            // 開始繪製
            this.paint.save();
            this.paint.rotate(deg * Math.PI / 180);
            this.paint[fontStyle](str[i], x, this.canvas.height / 2);
            this.paint.restore();
        }
    };
複製程式碼

自定義引數

到這裡,單次繪製基本完成,我們再回頭來看看有哪些允許自定義的引數。

    function Regcode(params = {}) {
        let p = Object.assign({
            lineWidth: 0.5,  // 線條寬度
            lineNum: 2,  // 線條數量
            dotNum: 10, // 點的數量
            dotR: 1, // 點的半徑
            foregroundColor: [10, 80], // 前景色區間
            backgroundColor: [150, 250], // 背景色區間
            fontSize: 20, // 字型大小
            fontFamily: 'Georgia', // 字型型別
            fontStyle: 'fill', // 字型繪製方法,fill/stroke
            content: 'acdefhijkmnpwxyABCDEFGHJKMNPQWXY12345789', // 驗證碼因子
            len: 4 // 驗證碼長度
        }, params);
        Object.keys(p).forEach(k => { // 將所有屬性組合後新增到this上
            this[k] = p[k];
        });
        this.canvas = null; // canvas dom
        this.paint = null; // canvas 2d
    }
複製程式碼

點選更新畫布

最開始分析需求的時候說過,需要點選可以更新驗證碼的功能,所以,現在還得加點東西。我們要更新畫布,首先要清空之前的畫布:

    Regcode.prototype.clear = function() { // 清空畫布
        this.paint.clearRect(0, 0, this.canvas.width, this.canvas.height);
    };
複製程式碼

清空之後,可以再次繪製以及 dom 點選事件的監聽。

   // 更新畫布
    Regcode.prototype.drawAgain = function() {
        this.clear();
        this.draw(this.callback);
    };
    
    // 監聽點選事件
    Regcode.prototype.draw = function(dom, callback = function () {}) { // 繪圖
        // 獲取canvas dom
        if (!this.paint) {
            ...
            ...
            // 回撥函式賦值給this,方便使用
            this.callback = callback;
            this.canvas.onclick = () => {
                this.drawAgain();
            }
        }
        ...
        ...
    }
複製程式碼

測試以及小結

現在,整個驗證碼就寫完了,當然需要測試一下:

    let reg = new Regcode(); // 不傳值,統一走預設值
    reg.draw(document.querySelector('#regcode'), r => {
        console.log(r); // WwB5
    });
    console.log(reg);
複製程式碼

看看列印出來的例項物件:

【2019 前端進階之路】JavaScript 原型和原型鏈及 canvas 驗證碼實踐
顯而易見的是例項物件擁有建構函式中定義好的屬性以及預設值,而且繼承了原型上的所有方法。

其實這種驗證碼的實現形式有很多,比如其實可以在例項化的時候就將所有的引數傳入。我們之前瞭解new命令的原理,所以知道其實在例項化物件的時候,會執行一遍建構函式。這樣,我們可以將 draw 方法和點選事件監聽一併放在建構函式中,也就不需要在外部再呼叫一次 draw 方法。

當然現在這種方式也有好處,就是足夠靈活。其實這些都只是使用上的小差別,大體思路是一致的。作者之前用 class 方式也寫過一版,在作者的開源作品 vchat 中的驗證碼,就是用的那一版。不過現在原型這一版,相對來說做了許多優化,有興趣的同學可以對比看一下。

手寫Promise

對於 Promise 的實現,網上有很多,作者之前也寫過一篇 站住,你這個Promise!。裡面有詳細的介紹和手寫流程,所以就不再贅敘了,有需要的同學可以前去看看。

相關文章

由衷感謝這些文章的作者。

交流群

qq前端交流群:960807765,歡迎各種技術交流,期待你的加入

公眾號

歡迎關注公眾號 前端發動機,江三瘋的前端二三事,專注技術,也會時常迷糊。希望在未來的前端路上,與你一同成長。

【2019 前端進階之路】JavaScript 原型和原型鏈及 canvas 驗證碼實踐

後記

如果你看到了這裡,且本文對你有一點幫助的話,希望你可以動動小手支援一下作者,感謝?。文中如有不對之處,也歡迎大家指出,共勉。

相關文章