您好,登錄后才能下訂單哦!
學習目標:
自己是個做了幾年開發的老碼農,希望本文對你有用! 這里推薦一下我的前端學習交流圈:767273102 ,里面都是學習前端的,從最基礎的HTML+CSS+JS【炫酷特效,游戲,插件封裝,設計模式】到移動端HTML5的項目實戰的學習資料都有整理,送給每一位前端小伙伴。2019最新技術,與企業需求同步。好友都在里面學習交流,每天都會有大牛定時講解前端技術!
在 JavaScript 中,所有數據類型都可以視為對象,當然也可以自定義對象。
自定義的對象數據類型就是面向對象中的類( Class )的概念。
我們以一個例子來說明面向過程和面向對象在程序流程上的不同之處。
假設我們要處理學生的成績表,為了表示一個學生的成績,面向過程的程序可以用一個對象表示:
var std1 = { name: 'Michael', score: 98 } var std2 = { name: 'Bob', score: 81 }
而處理學生成績可以通過函數實現,比如打印學生的成績:
function printScore (student) { console.log('姓名:' + student.name + ' ' + '成績:' + student.score) }
如果采用面向對象的程序設計思想,我們首選思考的不是程序的執行流程,
而是 Student 這種數據類型應該被視為一個對象,這個對象擁有 name 和 score 這兩個屬性(Property)。
如果要打印一個學生的成績,首先必須創建出這個學生對應的對象,然后,給對象發一個 printScore 消息,讓對象自己把自己的數據打印出來。
抽象數據行為模板(Class):
function Student (name, score) { this.name = name this.score = score } Student.prototype.printScore = function () { console.log('姓名:' + this.name + ' ' + '成績:' + this.score) }
根據模板創建具體實例對象(Instance):
var std1 = new Student('Michael', 98) var std2 = new Student('Bob', 81)
實例對象具有自己的具體行為(給對象發消息):
std1.printScore() // => 姓名:Michael 成績:98 std2.printScore() // => 姓名:Bob 成績 81
面向對象的設計思想是從自然界中來的,因為在自然界中,類(Class)和實例(Instance)的概念是很自然的。
Class 是一種抽象概念,比如我們定義的 Class——Student ,是指學生這個概念,
而實例(Instance)則是一個個具體的 Student ,比如, Michael 和 Bob 是兩個具體的 Student 。
所以,面向對象的設計思想是:
面向對象的抽象程度又比函數要高,因為一個 Class 既包含數據,又包含操作數據的方法。
我們可以直接通過 new Object() 創建:
var person = new Object() person.name = 'Jack' person.age = 18 person.sayName = function () { console.log(this.name) }
var person = { name: 'Jack', age: 18, sayName: function () { console.log(this.name) } }
對于上面的寫法固然沒有問題,但是假如我們要生成兩個 person 實例對象呢?
我們可以寫一個函數,解決代碼重復問題:
function createPerson (name, age) { return { name: name, age: age, sayName: function () { console.log(this.name) } } }
然后生成實例對象:
var p1 = createPerson('Jack', 18) var p2 = createPerson('Mike', 18)
這樣封裝確實爽多了,通過工廠模式我們解決了創建多個相似對象代碼冗余的問題,
但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
內容引導:
構造函數語法
分析構造函數
構造函數和實例對象的關系
普通函數調用和構造函數調用的區別
構造函數的返回值
構造函數的靜態成員和實例成員
構造函數的問題
一種更優雅的工廠函數就是下面這樣,構造函數:
function Person (name, age) { this.name = name this.age = age this.sayName = function () { console.log(this.name) } } var p1 = new Person('Jack', 18) p1.sayName() // => Jack var p2 = new Person('Mike', 23) p2.sayName() // => Mike
在上面的示例中,Person() 函數取代了 createPerson() 函數,但是實現效果是一樣的。
這是為什么呢?
我們注意到,Person() 中的代碼與 createPerson() 有以下幾點不同之處:
而要創建 Person 實例,則必須使用 new 操作符。
以這種方式調用構造函數會經歷以下 4 個步驟:
下面是具體的偽代碼:
function Person (name, age) { // 當使用 new 操作符調用 Person() 的時候,實際上這里會先創建一個對象 // var instance = {} // 然后讓內部的 this 指向 instance 對象 // this = instance // 接下來所有針對 this 的操作實際上操作的就是 instance this.name = name this.age = age this.sayName = function () { console.log(this.name) } // 在函數的結尾處會將 this 返回,也就是 instance // return this }
使用構造函數的好處不僅僅在于代碼的簡潔性,更重要的是我們可以識別對象的具體類型了。
在每一個實例對象中的 proto 中同時有一個 constructor 屬性,該屬性指向創建該實例的構造函數:
console.log(p1.constructor === Person) // => true console.log(p2.constructor === Person) // => true console.log(p1.constructor === p2.constructor) // => true
對象的 constructor 屬性最初是用來標識對象類型的,
但是,如果要檢測對象的類型,還是使用 instanceof 操作符更可靠一些:
console.log(p1 instanceof Person) // => true console.log(p2 instanceof Person) // => true
使用構造函數帶來的最大的好處就是創建對象更方便了,但是其本身也存在一個浪費內存的問題:
function Person (name, age) { this.name = name this.age = age this.type = 'human' this.sayHello = function () { console.log('hello ' + this.name) } } var p1 = new Person('lpz', 18) var p2 = new Person('Jack', 16)
在該示例中,從表面上好像沒什么問題,但是實際上這樣做,有一個很大的弊端。那就是對于每一個實例對象,type 和 sayHello 都是一模一樣的內容,每一次生成一個實例,都必須為重復的內容,多占用一些內存,如果實例對象很多,會造成極大的內存浪費。
console.log(p1.sayHello === p2.sayHello) // => false
對于這種問題我們可以把需要共享的函數定義到構造函數外部:
function sayHello = function () { console.log('hello ' + this.name) } function Person (name, age) { this.name = name this.age = age this.type = 'human' this.sayHello = sayHello } var p1 = new Person('lpz', 18) var p2 = new Person('Jack', 16) console.log(p1.sayHello === p2.sayHello) // => true
這樣確實可以了,但是如果有多個需要共享的函數的話就會造成全局命名空間沖突的問題。
你肯定想到了可以把多個函數放到一個對象中用來避免全局命名空間沖突的問題:
var fns = { sayHello: function () { console.log('hello ' + this.name) }, sayAge: function () { console.log(this.age) } } function Person (name, age) { this.name = name this.age = age this.type = 'human' this.sayHello = fns.sayHello this.sayAge = fns.sayAge } var p1 = new Person('lpz', 18) var p2 = new Person('Jack', 16) console.log(p1.sayHello === p2.sayHello) // => true console.log(p1.sayAge === p2.sayAge) // => true
至此,我們利用自己的方式基本上解決了構造函數的內存浪費問題。
小結
構造函數語法
分析構造函數
構造函數和實例對象的關系
構造函數的問題
內容引導:
使用 prototype 原型對象解決構造函數的問題
分析 構造函數、prototype 原型對象、實例對象 三者之間的關系
屬性成員搜索原則:原型鏈
實例對象讀寫原型對象中的成員
原型對象的簡寫形式
原生對象的原型
原型對象的問題
構造的函數和原型對象使用建議
Javascript 規定,每一個構造函數都有一個 prototype 屬性,指向另一個對象。
這個對象的所有屬性和方法,都會被構造函數的實例繼承。
這也就意味著,我們可以把所有對象實例需要共享的屬性和方法直接定義在 prototype 對象上。
function Person (name, age) { this.name = name this.age = age } console.log(Person.prototype) Person.prototype.type = 'human' Person.prototype.sayName = function () { console.log(this.name) } var p1 = new Person(...) var p2 = new Person(...) console.log(p1.sayName === p2.sayName) // => true
這時所有實例的 type 屬性和 sayName() 方法,
其實都是同一個內存地址,指向 prototype 對象,因此就提高了運行效率。
任何函數都具有一個 prototype 屬性,該屬性是一個對象。
function F () {} console.log(F.prototype) // => object F.prototype.sayHi = function () { console.log('hi!') }
構造函數的 prototype 對象默認都有一個 constructor 屬性,指向 prototype 對象所在函數。
console.log(F.constructor === F) // => true
通過構造函數得到的實例對象內部會包含一個指向構造函數的 prototype 對象的指針 proto 。
var instance = new F() console.log(instance.__proto__ === F.prototype) // => true
<p class="tip">
proto 是非標準屬性。
</p>
實例對象可以直接訪問原型對象成員。
instance.sayHi() // => hi!
總結:
了解了 構造函數-實例-原型對象 三者之間的關系后,接下來我們來解釋一下為什么實例對象可以訪問原型對象中的成員。
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性
也就是說,在我們調用 person1.sayName() 的時候,會先后執行兩次搜索:
而這正是多個對象實例共享原型所保存的屬性和方法的基本原理。
總結:
我們注意到,前面例子中每添加一個屬性和方法就要敲一遍 Person.prototype 。
為減少不必要的輸入,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象:
function Person (name, age) { this.name = name this.age = age } Person.prototype = { type: 'human', sayHello: function () { console.log('我叫' + this.name + ',我今年' + this.age + '歲了') } }
在該示例中,我們將 Person.prototype 重置到了一個新的對象。
這樣做的好處就是為 Person.prototype 添加成員簡單了,但是也會帶來一個問題,那就是原型對象丟失了 constructor 成員。
所以,我們為了保持 constructor 的指向正確,建議的寫法是:
function Person (name, age) { this.name = name this.age = age } Person.prototype = { constructor: Person, // => 手動將 constructor 指向正確的構造函數 type: 'human', sayHello: function () { console.log('我叫' + this.name + ',我今年' + this.age + '歲了') } }
<p class="tip">
所有函數都有 prototype 屬性對象。
</p>
練習:為數組對象和字符串對象擴展原型方法
function Person (name, age) { this.type = 'human' this.name = name this.age = age } function Student (name, age) { // 借用構造函數繼承屬性成員 Person.call(this, name, age) } var s1 = Student('張三', 18) console.log(s1.type, s1.name, s1.age) // => human 張三 18
function Person (name, age) { this.type = 'human' this.name = name this.age = age } Person.prototype.sayName = function () { console.log('hello ' + this.name) } function Student (name, age) { Person.call(this, name, age) } // 原型對象拷貝繼承原型對象成員 for(var key in Person.prototype) { Student.prototype[key] = Person.prototype[key] } var s1 = Student('張三', 18) s1.sayName() // => hello 張三
function Person (name, age) { this.type = 'human' this.name = name this.age = age } Person.prototype.sayName = function () { console.log('hello ' + this.name) } function Student (name, age) { Person.call(this, name, age) } // 利用原型的特性實現繼承 Student.prototype = new Person() var s1 = Student('張三', 18) console.log(s1.type) // => human s1.sayName() // => hello 張三
函數的調用方式決定了 this 指向的不同:
這就是對函數內部 this 指向的基本整理,寫代碼寫多了自然而然就熟悉了。
函數也是對象
call、apply、bind
那了解了函數 this 指向的不同場景之后,我們知道有些情況下我們為了使用某種特定環境的 this 引用,
這時候時候我們就需要采用一些特殊手段來處理了,例如我們經常在定時器外部備份 this 引用,然后在定時器函數內部使用外部 this 的引用。
然而實際上對于這種做法我們的 JavaScript 為我們專門提供了一些函數方法用來幫我們更優雅的處理函數內部 this 指向問題。
這就是接下來我們要學習的 call、apply、bind 三個函數方法。
call
call() 方法調用一個函數, 其具有一個指定的 this 值和分別地提供的參數(參數的列表)。
<p class="danger">
注意:該方法的作用和 apply() 方法類似,只有一個區別,就是 call() 方法接受的是若干個參數的列表,而 apply() 方法接受的是一個包含多個參數的數組。
</p>
語法:
fun.call(thisArg[, arg1[, arg2[, ...]]])
參數:
thisArg
arg1, arg2, ...
apply
apply() 方法調用一個函數, 其具有一個指定的 this 值,以及作為一個數組(或類似數組的對象)提供的參數。
<p class="danger">
注意:該方法的作用和 call() 方法類似,只有一個區別,就是 call() 方法接受的是若干個參數的列表,而 apply() 方法接受的是一個包含多個參數的數組。
</p>
語法:
fun.apply(thisArg, [argsArray])
參數:
apply() 與 call() 非常相似,不同之處在于提供參數的方式。
apply() 使用參數數組而不是一組參數列表。例如:
fun.apply(this, ['eat', 'bananas'])
bind
bind() 函數會創建一個新函數(稱為綁定函數),新函數與被調函數(綁定函數的目標函數)具有相同的函數體(在 ECMAScript 5 規范中內置的call屬性)。
當目標函數被調用時 this 值綁定到 bind() 的第一個參數,該參數不能被重寫。綁定函數被調用時,bind() 也接受預設的參數提供給原函數。
一個綁定函數也能使用new操作符創建對象:這種行為就像把原函數當成構造器。提供的 this 值被忽略,同時調用時的參數被提供給模擬函數。
語法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
參數:
thisArg
arg1, arg2, ...
返回值:
返回由指定的this值和初始化參數改造的原函數拷貝。
小結
call 和 apply 特性一樣
bind
可以用來指定內部 this 的指向,然后生成一個改變了 this 指向的新的函數
它和 call、apply 最大的區別是:bind 不會調用
bind 支持傳遞參數,它的傳參方式比較特殊,一共有兩個位置可以傳遞
arguments
caller
length
name
function fn(x, y, z) { console.log(fn.length) // => 形參的個數 console.log(arguments) // 偽數組實參參數集合 console.log(arguments.callee === fn) // 函數本身 console.log(fn.caller) // 函數的調用者 console.log(fn.name) // => 函數的名字 } function f() { fn(10, 20, 30) } f()
閉包就是能夠讀取其他函數內部變量的函數,
由于在 Javascript 語言中,只有函數內部的子函數才能讀取局部變量,
因此可以把閉包簡單理解成 “定義在一個函數內部的函數”。
所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
閉包的用途:
一些關于閉包的例子
示例1:
var arr = [10, 20, 30] for(var i = 0; i < arr.length; i++) { arr[i] = function () { console.log(i) } }
示例2:
console.log(111) for(var i = 0; i < 3; i++) { setTimeout(function () { console.log(i) }, 0) } console.log(222)
正則表達式是對字符串操作的一種邏輯公式,就是用事先定義好的一些特定字符、及這些特定字符的組合,組成一個“規則字符串”,這個“規則字符串”用來表達對字符串的一種過濾邏輯。
方式1:
var reg = new Regex('\d', 'i'); var reg = new Regex('\d', 'gi');
方式2:
var reg = /\d/i; var reg = /\d/gi;
// 1\. 提取工資 var str = "張三:1000,李四:5000,王五:8000。"; var array = str.match(/\d+/g); console.log(array); // 2\. 提取email地址 var str = "123123@xx.com,fangfang@valuedopinions.cn 286669312@qq.com 2、emailenglish@emailenglish.englishtown.com 286669312@qq.com..."; var array = str.match(/\w+@\w+\.\w+(\.\w+)?/g); console.log(array); // 3\. 分組提取 // 3\. 提取日期中的年部分 2015-5-10var dateStr = '2016-1-5'; // 正則表達式中的()作為分組來使用,獲取分組匹配到的結果用Regex.$1 $2 $3....來獲取 var reg = /(\d{4})-\d{1,2}-\d{1,2}/; if (reg.test(dateStr)) { console.log(RegExp.$1);} // 4\. 提取郵件中的每一部分 var reg = /(\w+)@(\w+)\.(\w+)(\.\w+)?/; var str = "123123@xx.com"; if (reg.test(str)) { console.log(RegExp.$1); console.log(RegExp.$2); console.log(RegExp.$3);}
// 1\. 替換所有空白 var str = " 123AD asadf asadfasf adf "; str = str.replace(/\s/g,"xx"); console.log(str); // 2\. 替換所有,|, var str = "abc,efg,123,abc,123,a"; str = str.replace(/,|,/g, "."); console.log(str);
QQ號:<input type="text" id="txtQQ"><span></span><br> 郵箱:<input type="text" id="txtEMail"><span></span><br> 手機:<input type="text" id="txtPhone"><span></span><br> 生日:<input type="text" id="txtBirthday"><span></span><br> 姓名:<input type="text" id="txtName"><span></span><br>
//獲取文本框 var txtQQ = document.getElementById("txtQQ"); var txtEMail = document.getElementById("txtEMail"); var txtPhone = document.getElementById("txtPhone"); var txtBirthday = document.getElementById("txtBirthday"); var txtName = document.getElementById("txtName"); // txtQQ.onblur = function () { //獲取當前文本框對應的span var span = this.nextElementSibling; var reg = /^\d{5,12}$/; //判斷驗證是否成功 if(!reg.test(this.value) ){ //驗證不成功 span.innerText = "請輸入正確的QQ號"; span.style.color = "red"; }else{ //驗證成功 span.innerText = ""; span.style.color = ""; } }; //txtEMail txtEMail.onblur = function () { //獲取當前文本框對應的span var span = this.nextElementSibling; var reg = /^\w+@\w+\.\w+(\.\w+)?$/; //判斷驗證是否成功 if(!reg.test(this.value) ){ //驗證不成功 span.innerText = "請輸入正確的EMail地址"; span.style.color = "red"; }else{ //驗證成功 span.innerText = ""; span.style.color = ""; } };
表單驗證部分,封裝成函數:
var regBirthday = /^\d{4}-\d{1,2}-\d{1,2}$/; addCheck(txtBirthday, regBirthday, "請輸入正確的出生日期"); //給文本框添加驗證 function addCheck(element, reg, tip) { element.onblur = function () { //獲取當前文本框對應的span var span = this.nextElementSibling; //判斷驗證是否成功 if(!reg.test(this.value) ){ //驗證不成功 span.innerText = tip; span.style.color = "red"; }else{ //驗證成功 span.innerText = ""; span.style.color = ""; } }; }
通過給元素增加自定義驗證屬性對表單進行驗證:
<form id="frm"> QQ號:<input type="text" name="txtQQ" data-rule="qq"><span></span><br> 郵箱:<input type="text" name="txtEMail" data-rule="email"><span></span><br> 手機:<input type="text" name="txtPhone" data-rule="phone"><span></span><br> 生日:<input type="text" name="txtBirthday" data-rule="date"><span></span><br> 姓名:<input type="text" name="txtName" data-rule="cn"><span></span><br> </form>
// 所有的驗證規則 var rules = [ { name: 'qq', reg: /^\d{5,12}$/, tip: "請輸入正確的QQ" }, { name: 'email', reg: /^\w+@\w+\.\w+(\.\w+)?$/, tip: "請輸入正確的郵箱地址" }, { name: 'phone', reg: /^\d{11}$/, tip: "請輸入正確的手機號碼" }, { name: 'date', reg: /^\d{4}-\d{1,2}-\d{1,2}$/, tip: "請輸入正確的出生日期" }, { name: 'cn', reg: /^[\u4e00-\u9fa5]{2,4}$/, tip: "請輸入正確的姓名" }]; addCheck('frm'); //給文本框添加驗證 function addCheck(formId) { var i = 0, len = 0, frm =document.getElementById(formId); len = frm.children.length; for (; i < len; i++) { var element = frm.children[i]; // 表單元素中有name屬性的元素添加驗證 if (element.name) { element.onblur = function () { // 使用dataset獲取data-自定義屬性的值 var ruleName = this.dataset.rule; var rule =getRuleByRuleName(rules, ruleName); var span = this.nextElementSibling; //判斷驗證是否成功 if(!rule.reg.test(this.value) ){ //驗證不成功 span.innerText = rule.tip; span.style.color = "red"; }else{ //驗證成功 span.innerText = ""; span.style.color = ""; } } } } } // 根據規則的名稱獲取規則對象 function getRuleByRuleName(rules, ruleName) { var i = 0, len = rules.length; var rule = null; for (; i < len; i++) { if (rules[i].name == ruleName) { rule = rules[i]; break; } } return rule; }
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。