學習目標:- 理解面向對象開發思想- 掌握 JavaScript 面向對象開發相關模式- 掌握在 JavaScript 中使用正則表達式- typora-copy-images-to media
JavaScript 高級
?
課程介紹
課程大綱
在線地址:JavaScript 高級
目標
-
理解面向對象開發思想
-
掌握 JavaScript 面向對象開發相關模式
-
掌握在 JavaScript 中使用正則表達式
案例演示
-
貪吃蛇
基本概念復習
由于 JavaScript 高級還是針對 JavaScript 語言本身的一個進階學習,所以在開始之前我們先對以前所學過的 JavaScript 相關知識點做一個快速復習總結。
重新介紹 JavaScript
JavaScript 是什么
-
解析執行:輕量級解釋型的,或是 JIT 編譯型的程序設計語言
-
語言特點:動態,頭等函數 (First-class Function)
-
又稱函數是 JavaScript 中的一等公民
-
-
執行環境:在宿主環境(host environment)下運行,瀏覽器是最常見的 JavaScript 宿主環境
-
但是在很多非瀏覽器環境中也使用 JavaScript ,例如 node.js
-
-
編程范式:基于原型、多范式的動態腳本語言,并且支持面向對象、命令式和聲明式(如:函數式編程)編程風格
JavaScript 與瀏覽器的關系
?
JavaScript 的組成
組成部分 | 說明 |
---|---|
Ecmascript | 描述了該語言的語法和基本對象 |
DOM | 描述了處理網頁內容的方法和接口 |
BOM | 描述了與瀏覽器進行交互的方法和接口 |
JavaScript 可以做什么
Any application that can be written in JavaScript, will eventually be written in JavaScript. 凡是能用 JavaScript 寫出來的,最終都會用 JavaScript 寫出來
-
知乎 - JavaScript 能做什么,該做什么?
-
最流行的編程語言 JavaScript 能做什么?
JavaScript 發展歷史
JavaScript 標準參考教程 - JavaScript 語言的歷史
-
JavaScript 的誕生
-
JavaScript 與 Ecmascript 的關系
-
JavaScript 與 Java 的關系
-
JavaScript 的版本
-
JavaScript 周邊大事記
小結
基本概念
本小節快速過即可,主要是對學過的內容做知識點梳理。
-
語法
-
區分大小寫
-
標識符
-
注釋
-
嚴格模式
-
語句
-
-
關鍵字和保留字
-
變量
-
數據類型
-
typeof 操作符
-
Undefined
-
Null
-
Boolean
-
Number
-
String
-
Object
-
-
操作符
-
流程控制語句
-
函數
JavaScript 中的數據類型
JavaScript 有 5 種簡單數據類型:Undefined、Null、Boolean、Number、String
和 1 種復雜數據類型 Object
。
基本類型(值類型)
-
Undefined
-
Null
-
Boolean
-
Number
-
String
復雜類型(引用類型)
-
Object
-
Array
-
Date
-
RegExp
-
Function
-
基本包裝類型
-
Boolean
-
Number
-
String
-
-
單體內置對象
-
Global
-
Math
-
類型檢測
-
typeof
-
instanceof
-
Object.prototype.toString.call()
值類型和引用類型在內存中的存儲方式(畫圖說明)
-
值類型按值存儲
-
引用類型按引用存儲
值類型復制和引用類型復制(畫圖說明)
-
值類型按值復制
-
引用類型按引用復制
值類型和引用類型參數傳遞(畫圖說明)
-
值類型按值傳遞
-
引用類型按引用傳遞
值類型與引用類型的差別
-
基本類型在內存中占據固定大小的空間,因此被保存在棧內存中
-
從一個變量向另一個變量復制基本類型的值,復制的是值的副本
-
引用類型的值是對象,保存在堆內存
-
包含引用類型值的變量實際上包含的并不是對象本身,而是一個指向該對象的指針
-
從一個變量向另一個變量復制引用類型的值的時候,復制是引用指針,因此兩個變量最終都指向同一個對象
小結
-
類型檢測方式
-
值類型和引用類型的存儲方式
-
值類型復制和引用類型復制
-
方法參數中 值類型數據傳遞 和 引用類型數據傳遞
JavaScript 執行過程
JavaScript 運行分為兩個階段:
-
預解析
-
全局預解析(所有變量和函數聲明都會提前;同名的函數和變量函數的優先級高)
-
函數內部預解析(所有的變量、函數和形參都會參與預解析)
-
函數
-
形參
-
普通變量
-
-
-
執行
先預解析全局作用域,然后執行全局作用域中的代碼,在執行全局代碼的過程中遇到函數調用就會先進行函數預解析,然后再執行函數內代碼。
JavaScript 面向對象編程
?
面向對象介紹
什么是對象
Everything is object (萬物皆對象)
?
對象到底是什么,我們可以從兩次層次來理解。
(1) 對象是單個事物的抽象。
一本書、一輛汽車、一個人都可以是對象,一個數據庫、一張網頁、一個與遠程服務器的連接也可以是對象。當實物被抽象成對象,實物之間的關系就變成了對象之間的關系,從而就可以模擬現實情況,針對對象進行編程。
(2) 對象是一個容器,封裝了屬性(property)和方法(method)。
屬性是對象的狀態,方法是對象的行為(完成某種任務)。比如,我們可以把動物抽象為animal對象,使用“屬性”記錄具體是那一種動物,使用“方法”表示動物的某種行為(奔跑、捕獵、休息等等)。
在實際開發中,對象是一個抽象的概念,可以將其簡單理解為:數據集或功能集。
ECMAScript-262 把對象定義為:無序屬性的集合,其屬性可以包含基本值、對象或者函數。嚴格來講,這就相當于說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都映射到一個值。
<p class="tip"> 提示:每個對象都是基于一個引用類型創建的,這些類型可以是系統內置的原生類型,也可以是開發人員自定義的類型。</p>
什么是面向對象
面向對象不是新的東西,它只是過程式代碼的一種高度封裝,目的在于提高代碼的開發效率和可維護性。
?
面向對象編程 —— Object Oriented Programming,簡稱 OOP ,是一種編程開發思想。它將真實世界各種復雜的關系,抽象為一個個對象,然后由對象之間的分工與合作,完成對真實世界的模擬。
在面向對象程序開發思想中,每一個對象都是功能中心,具有明確分工,可以完成接受信息、處理數據、發出信息等任務。因此,面向對象編程具有靈活、代碼可復用、高度模塊化等特點,容易維護和開發,比起由一系列函數或指令組成的傳統的過程式編程(procedural programming),更適合多人合作的大型軟件項目。
面向對象與面向過程:
-
面向過程就是親力親為,事無巨細,面面俱到,步步緊跟,有條不紊
-
面向對象就是找一個對象,指揮得結果
-
面向對象將執行者轉變成指揮者
-
面向對象不是面向過程的替代,而是面向過程的封裝
面向對象的特性:
-
封裝性
-
繼承性
-
[多態性]
擴展閱讀:
-
維基百科 - 面向對象程序設計
-
知乎:如何用一句話說明什么是面向對象思想?
-
知乎:什么是面向對象編程思想?
程序中面向對象的基本體現
在 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
-
根據 Class 創建 Instance
-
指揮 Instance 得結果
面向對象的抽象程度又比函數要高,因為一個 Class 既包含數據,又包含操作數據的方法。
創建對象
簡單方式
我們可以直接通過 new Object()
創建:
var person = new Object()
person.name = 'Jack'
person.age = 18
?
person.sayName = function () {
?console.log(this.name)
}
每次創建通過 new Object()
比較麻煩,所以可以通過它的簡寫形式對象字面量來創建:
var person = {
?name: 'Jack',
?age: 18,
?sayName: function () {
? ?console.log(this.name)
}
}
對于上面的寫法固然沒有問題,但是假如我們要生成兩個 person
實例對象呢?
var person1 = {
?name: 'Jack',
?age: 18,
?sayName: function () {
? ?console.log(this.name)
}
}
?
var person2 = {
?name: 'Mike',
?age: 16,
?sayName: function () {
? ?console.log(this.name)
}
}
通過上面的代碼我們不難看出,這樣寫的代碼太過冗余,重復性太高。
簡單方式的改進:工廠函數
我們可以寫一個函數,解決代碼重復問題:
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)
這樣封裝確實爽多了,通過工廠模式我們解決了創建多個相似對象代碼冗余的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
構造函數
內容引導:
-
構造函數語法
-
分析構造函數
-
構造函數和實例對象的關系
-
實例的 constructor 屬性
-
instanceof 操作符
-
-
普通函數調用和構造函數調用的區別
-
構造函數的返回值
-
構造函數的靜態成員和實例成員
-
函數也是對象
-
實例成員
-
靜態成員
-
-
構造函數的問題
更優雅的工廠函數:構造函數
一種更優雅的工廠函數就是下面這樣,構造函數:
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()
有以下幾點不同之處:
-
沒有顯示的創建對象
-
直接將屬性和方法賦給了
this
對象 -
沒有
return
語句 -
函數名使用的是大寫的
Person
而要創建 Person
實例,則必須使用 new
操作符。以這種方式調用構造函數會經歷以下 4 個步驟:
-
創建一個新對象
-
將構造函數的作用域賦給新對象(因此 this 就指向了這個新對象)
-
執行構造函數中的代碼
-
返回新對象
下面是具體的偽代碼:
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
總結:
-
構造函數是根據具體的事物抽象出來的抽象模板
-
實例對象是根據抽象的構造函數模板得到的具體實例對象
-
每一個實例對象都具有一個
constructor
屬性,指向創建該實例的構造函數-
注意:
constructor
是實例的屬性的說法不嚴謹,具體后面的原型會講到
-
-
可以通過實例的
constructor
屬性判斷實例和構造函數之間的關系-
注意:這種方式不嚴謹,推薦使用
instanceof
操作符,后面學原型會解釋為什么
-
構造函數的問題
使用構造函數帶來的最大的好處就是創建對象更方便了,但是其本身也存在一個浪費內存的問題:
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
至此,我們利用自己的方式基本上解決了構造函數的內存浪費問題。但是代碼看起來還是那么的格格不入,那有沒有更好的方式呢?
小結
-
構造函數語法
-
分析構造函數
-
構造函數和實例對象的關系
-
實例的 constructor 屬性
-
instanceof 操作符
-
-
構造函數的問題
原型
內容引導:
-
使用 prototype 原型對象解決構造函數的問題
-
分析 構造函數、prototype 原型對象、實例對象 三者之間的關系
-
屬性成員搜索原則:原型鏈
-
實例對象讀寫原型對象中的成員
-
原型對象的簡寫形式
-
原生對象的原型
-
Object
-
Array
-
String
-
...
-
-
原型對象的問題
-
構造的函數和原型對象使用建議
更好的解決方案: 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(
這時所有實例的 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!
總結:
-
任何函數都具有一個
prototype
屬性,該屬性是一個對象 -
構造函數的
prototype
對象默認都有一個constructor
屬性,指向prototype
對象所在函數 -
通過構造函數得到的實例對象內部會包含一個指向構造函數的
prototype
對象的指針__proto__
-
所有實例都直接或間接繼承了原型對象的成員
屬性成員的搜索原則:原型鏈
了解了 構造函數-實例-原型對象 三者之間的關系后,接下來我們來解釋一下為什么實例對象可以訪問原型對象中的成員。
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性
-
搜索首先從對象實例本身開始
-
如果在實例中找到了具有給定名字的屬性,則返回該屬性的值
-
如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性
-
如果在原型對象中找到了這個屬性,則返回該屬性的值
也就是說,在我們調用 person1.sayName()
的時候,會先后執行兩次搜索:
-
首先,解析器會問:“實例 person1 有 sayName 屬性嗎?”答:“沒有。
-
”然后,它繼續搜索,再問:“ person1 的原型有 sayName 屬性嗎?”答:“有。
-
”于是,它就讀取那個保存在原型對象中的函數。
-
當我們調用 person2.sayName() 時,將會重現相同的搜索過程,得到相同的結果。
而這正是多個對象實例共享原型所保存的屬性和方法的基本原理。
總結:
-
先在自己身上找,找到即返回
-
自己身上找不到,則沿著原型鏈向上查找,找到即返回
-
如果一直到原型鏈的末端還沒有找到,則返回
undefined
實例對象讀寫原型對象成員
讀取:
-
先在自己身上找,找到即返回
-
自己身上找不到,則沿著原型鏈向上查找,找到即返回
-
如果一直到原型鏈的末端還沒有找到,則返回
undefined
值類型成員寫入(實例對象.值類型成員 = xx
):
-
當實例期望重寫原型對象中的某個普通數據成員時實際上會把該成員添加到自己身上
-
也就是說該行為實際上會屏蔽掉對原型對象成員的訪問
引用類型成員寫入(實例對象.引用類型成員 = xx
):
-
同上
復雜類型修改(實例對象.成員.xx = xx
):
-
同樣會先在自己身上找該成員,如果自己身上找到則直接修改
-
如果自己身上找不到,則沿著原型鏈繼續查找,如果找到則修改
-
如果一直到原型鏈的末端還沒有找到該成員,則報錯(
實例對象.undefined.xx = xx
)
更簡單的原型語法
我們注意到,前面例子中每添加一個屬性和方法就要敲一遍 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>
-
Object.prototype
-
Function.prototype
-
Array.prototype
-
String.prototype
-
Number.prototype
-
Date.prototype
-
...
練習:為數組對象和字符串對象擴展原型方法。
原型對象的問題
-
共享數組
-
共享對象
如果真的希望可以被實例對象之間共享和修改這些共享數據那就不是問題。但是如果不希望實例之間共享和修改這些共享數據則就是問題。
一個更好的建議是,最好不要讓實例之間互相共享這些數組或者對象成員,一旦修改的話會導致數據的走向很不明確而且難以維護。
原型對象使用建議
-
私有成員(一般就是非函數成員)放到構造函數中
-
共享成員(一般就是函數)放到原型對象中
-
如果重置了
prototype
記得修正constructor
的指向
案例:隨機方塊
面向對象游戲案例:貪吃蛇
案例相關源碼以上傳到 GitHub :https://github.com/lipengzhou/new-snake
案例介紹
游戲演示
在線演示地址:貪吃蛇
案例目標
游戲的目的是用來體會js高級語法的使用 不需要具備抽象對象的能力,使用面向對象的方式分析問題,需要一個漫長的過程。
功能實現
搭建頁面
放一個容器盛放游戲場景 div#map,設置樣式
#map {
?width: 800px;
?height: 600px;
?background-color: #ccc;
?position: relative;
}
分析對象
-
游戲對象
-
蛇對象
-
食物對象
創建食物對象
-
Food
-
屬性
-
x
-
y
-
width
-
height
-
color
-
-
方法
-
render 隨機創建一個食物對象,并輸出到map上
-
-
-
創建Food的構造函數,并設置屬性
var position = 'absolute';
var elements = [];
function Food(x, y, width, height, color) {
?this.x = x || 0;
?this.y = y || 0;
?// 食物的寬度和高度(像素)
?this.width = width || 20;
?this.height = height || 20;
?// 食物的顏色
?this.color = color || 'green';
}
-
通過原型設置render方法,實現隨機產生食物對象,并渲染到map上
Food.prototype.render = function (map) {
?// 隨機食物的位置,map.寬度/food.寬度,總共有多少分food的寬度,隨機一下。然后再乘以food的寬度
?this.x = parseInt(Math.random() * map.offsetWidth / this.width) * this.width;
?this.y = parseInt(Math.random() * map.offsetHeight / this.height) * this.height;
?
?// 動態創建食物對應的div
?var div = document.createElement('div');
?map.appendChild(div);
?div.style.position = position;
?div.style.left = this.x + 'px';
?div.style.top = this.y + 'px';
?div.style.width = this.width + 'px';
?div.style.height = this.height + 'px';
?div.style.backgroundColor = this.color;
?elements.push(div);
}
-
通過自調用函數,進行封裝,通過window暴露Food對象
window.Food = Food;
創建蛇對象
-
Snake
-
屬性
-
width 蛇節的寬度 默認20
-
height 蛇節的高度 默認20
-
body 數組,蛇的頭部和身體,第一個位置是蛇頭
-
direction 蛇運動的方向 默認right 可以是 left top bottom
-
-
方法
-
render 把蛇渲染到map上
-
-
Snake構造函數
var position = 'absolute';
var elements = [];
function Snake(width, height, direction) {
?// 設置每一個蛇節的寬度
?this.width = width || 20;
?this.height = height || 20;
?// 蛇的每一部分, 第一部分是蛇頭
?this.body = [
? {x: 3, y: 2, color: 'red'},
? {x: 2, y: 2, color: 'red'},
? {x: 1, y: 2, color: 'red'}
];
?this.direction = direction || 'right';
}
-
render方法
Snake.prototype.render = function(map) {
?for(var i = 0; i < this.body.length; i++) {
? ?var obj = this.body[i];
? ?var div = document.createElement('div');
? ?map.appendChild(div);
? ?div.style.left = obj.x * this.width + 'px';
? ?div.style.top = obj.y * this.height + 'px';
? ?div.style.position = position;
? ?div.style.backgroundColor = obj.color;
? ?div.style.width = this.width + 'px';
? ?div.style.height = this.height + 'px';
}
}
-
在自調用函數中暴露Snake對象
window.Snake = Snake;
創建游戲對象
游戲對象,用來管理游戲中的所有對象和開始游戲
-
Game
-
屬性
-
food
-
snake
-
map
-
-
方法
-
start 開始游戲(繪制所有游戲對象)
-
-
?
-
構造函數
function Game(map) {
?this.food = new Food();
?this.snake = new Snake();
?this.map = map;
}
-
開始游戲,渲染食物對象和蛇對象
Game.prototype.start = function () {
?this.food.render(this.map);
?this.snake.render(this.map);
}
游戲的邏輯
寫蛇的move方法
-
在蛇對象(snake.js)中,在Snake的原型上新增move方法
-
讓蛇移動起來,把蛇身體的每一部分往前移動一下
-
蛇頭部分根據不同的方向決定 往哪里移動
Snake.prototype.move = function (food, map) {
?// 讓蛇身體的每一部分往前移動一下
?var i = this.body.length - 1;
?for(; i > 0; i--) {
? ?this.body[i].x = this.body[i - 1].x;
? ?this.body[i].y = this.body[i - 1].y;
}
?// 根據移動的方向,決定蛇頭如何處理
?switch(this.direction) {
? ?case 'left':
? ? ?this.body[0].x -= 1;
? ? ?break;
? ?case 'right':
? ? ?this.body[0].x += 1;
? ? ?break;
? ?case 'top':
? ? ?this.body[0].y -= 1;
? ? ?break;
? ?case 'bottom':
? ? ?this.body[0].y += 1;
? ? ?break;
}
}
-
在game中測試
this.snake.move(this.food, this.map);
this.snake.render(this.map);
讓蛇自己動起來
-
私有方法
什么是私有方法?不能被外部訪問的方法 如何創建私有方法?使用自調用函數包裹
-
在game.js中 添加runSnake的私有方法,開啟定時器調用蛇的move和render方法,讓蛇動起來
-
判斷蛇是否撞墻
function runSnake() {
?var timerId = setInterval(function() {
? ?this.snake.move(this.food, this.map);
? ?// 在渲染前,刪除之前的蛇
? ?this.snake.render(this.map);
?
? ?// 判斷蛇是否撞墻
? ?var maxX = this.map.offsetWidth / this.snake.width;
? ?var maxY = this.map.offsetHeight / this.snake.height;
? ?var headX = this.snake.body[0].x;
? ?var headY = this.snake.body[0].y;
? ?if (headX < 0 || headX >= maxX) {
? ? ?clearInterval(timerId);
? ? ?alert('Game Over');
? }
?
? ?if (headY < 0 || headY >= maxY) {
? ? ?clearInterval(timerId);
? ? ?alert('Game Over');
? }
?
}.bind(that), 150);
}
-
在snake中添加刪除蛇的私有方法,在render中調用
function remove() {
?// 刪除渲染的蛇
?var i = elements.length - 1;
?for(; i >= 0; i--) {
? ?// 刪除頁面上渲染的蛇
? ?elements[i].parentNode.removeChild(elements[i]);
? ?// 刪除elements數組中的元素
? ?elements.splice(i, 1);
}
}
-
在game中通過鍵盤控制蛇的移動方向
function bindKey() {
?document.addEventListener('keydown', function(e) {
? ?switch (e.keyCode) {
? ? ?case 37:
? ? ? ?// left
? ? ? ?this.snake.direction = 'left';
? ? ? ?break;
? ? ?case 38:
? ? ? ?// top
? ? ? ?this.snake.direction = 'top';
? ? ? ?break;
? ? ?case 39:
? ? ? ?// right
? ? ? ?this.snake.direction = 'right';
? ? ? ?break;
? ? ?case 40:
? ? ? ?// bottom
? ? ? ?this.snake.direction = 'bottom';
? ? ? ?break;
? }
}.bind(that), false);
}
-
在start方法中調用
bindKey();
判斷蛇是否吃到食物
// 在Snake的move方法中
?
// 在移動的過程中判斷蛇是否吃到食物
// 如果蛇頭和食物的位置重合代表吃到食物
// 食物的坐標是像素,蛇的坐標是幾個寬度,進行轉換
var headX = this.body[0].x * this.width;
var headY = this.body[0].y * this.height;
if (headX === food.x && headY === food.y) {
?// 吃到食物,往蛇節的最后加一節
?var last = this.body[this.body.length - 1];
?this.body.push({
? ?x: last.x,
? ?y: last.y,
? ?color: last.color
})
?// 把現在的食物對象刪除,并重新隨機渲染一個食物對象
?food.render(map);
}
其它處理
把html中的js代碼放到index.js中
避免html中出現js代碼
自調用函數的參數
(function (window, undefined) {
?var document = window.document;
?
}(window, undefined))
-
傳入window對象
將來代碼壓縮的時候,可以吧 function (window) 壓縮成 function (w)
-
傳入undefined
在將來會看到別人寫的代碼中會把undefined作為函數的參數(當前案例沒有使用)因為在有的老版本的瀏覽器中 undefined可以被重新賦值,防止undefined 被重新賦值
整理代碼
現在的代碼結構清晰,誰出問題就找到對應的js文件即可。通過自調用函數,已經防止了變量命名污染的問題
但是,由于js文件數較多,需要在頁面上引用,會產生文件依賴的問題(先引入那個js,再引入哪個js)將來通過工具把js文件合并并壓縮。現在手工合并js文件演示
-
問題1
// 如果存在多個自調用函數要用分號分割,否則語法錯誤
// 下面代碼會報錯
(function () {
}())
?
(function () {
}())
// 所以代碼規范中會建議在自調用函數之前加上分號
// 下面代碼沒有問題
;(function () {
}())
?
;(function () {
}())
-
問題2
// 當自調用函數 前面有函數聲明時,會把自調用函數作為參數
// 所以建議自調用函數前,加上;
var a = function () {
?alert('11');
}
? ?
(function () {
?alert('22');
}())
繼承
什么是繼承
-
現實生活中的繼承
-
程序中的繼承
構造函數的屬性繼承:借用構造函數
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
構造函數的原型方法繼承:拷貝繼承(for-in)
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 張三
函數進階
函數的定義方式
-
函數聲明
-
函數表達式
-
new Function
函數聲明
function foo () {
?
}
函數表達式
var foo = function () {
?
}
函數聲明與函數表達式的區別
-
函數聲明必須有名字
-
函數聲明會函數提升,在預解析階段就已創建,聲明前后都可以調用
-
函數表達式類似于變量賦值
-
函數表達式可以沒有名字,例如匿名函數
-
函數表達式沒有變量提升,在執行階段創建,必須在表達式執行之后才可以調用
下面是一個根據條件定義函數的例子:
if (true) {
?function f () {
? ?console.log(1)
}
} else {
?function f () {
? ?console.log(2)
}
}
以上代碼執行結果在不同瀏覽器中結果不一致。
不過我們可以使用函數表達式解決上面的問題:
var f
?
if (true) {
?f = function () {
? ?console.log(1)
}
} else {
?f = function () {
? ?console.log(2)
}
}
函數的調用方式
-
普通函數
-
構造函數
-
對象方法
函數內 this
指向的不同場景
函數的調用方式決定了 this
指向的不同:
調用方式 | 非嚴格模式 | 備注 |
---|---|---|
普通函數調用 | window | 嚴格模式下是 undefined |
構造函數調用 | 實例對象 | 原型方法中 this 也是實例對象 |
對象方法調用 | 該方法所屬對象 | 緊挨著的對象 |
事件綁定方法 | 綁定事件對象 | ? |
定時器函數 | window | ? |
這就是對函數內部 this 指向的基本整理,寫代碼寫多了自然而然就熟悉了。
函數也是對象
-
所有函數都是
Function
的實例
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
-
在 fun 函數運行時指定的 this 值
-
如果指定了 null 或者 undefined 則內部 this 指向 window
-
-
arg1, arg2, ...
-
指定的參數列表
-
apply
apply()
方法調用一個函數, 其具有一個指定的 this
值,以及作為一個數組(或類似數組的對象)提供的參數。
<p class="danger"> 注意:該方法的作用和 call()
方法類似,只有一個區別,就是 call()
方法接受的是若干個參數的列表,而 apply()
方法接受的是一個包含多個參數的數組。</p>
語法:
fun.apply(thisArg, [argsArray])
參數:
-
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
-
當綁定函數被調用時,該參數會作為原函數運行時的 this 指向。當使用new 操作符調用綁定函數時,該參數無效。
-
-
arg1, arg2, ...
-
當綁定函數被調用時,這些參數將置于實參之前傳遞給被綁定的方法。
-
返回值:
返回由指定的this值和初始化參數改造的原函數拷貝。
示例1:
this.x = 9;
var module = {
?x: 81,
?getX: function() { return this.x; }
};
?
module.getX(); // 返回 81
?
var retrieveX = module.getX;
retrieveX(); // 返回 9, 在這種情況下,"this"指向全局作用域
?
// 創建一個新函數,將"this"綁定到module對象
// 新手可能會被全局的x變量和module里的屬性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81
示例2:
function LateBloomer() {
?this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
?
// Declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
?window.setTimeout(this.declare.bind(this), 1000);
};
?
LateBloomer.prototype.declare = function() {
?console.log('I am a beautiful flower with ' +
? ?this.petalCount + ' petals!');
};
?
var flower = new LateBloomer();
flower.bloom(); ?// 一秒鐘后, 調用'declare'方法
小結
-
call 和 apply 特性一樣
-
都是用來調用函數,而且是立即調用
-
但是可以在調用函數的同時,通過第一個參數指定函數內部
this
的指向 -
call 調用的時候,參數必須以參數列表的形式進行傳遞,也就是以逗號分隔的方式依次傳遞即可
-
apply 調用的時候,參數必須是一個數組,然后在執行的時候,會將數組內部的元素一個一個拿出來,與形參一一對應進行傳遞
-
如果第一個參數指定了
null
或者undefined
則內部 this 指向 window
-
-
bind
-
可以用來指定內部 this 的指向,然后生成一個改變了 this 指向的新的函數
-
它和 call、apply 最大的區別是:bind 不會調用
-
bind 支持傳遞參數,它的傳參方式比較特殊,一共有兩個位置可以傳遞
-
在 bind 的同時,以參數列表的形式進行傳遞
-
-
在調用的時候,以參數列表的形式進行傳遞
-
-
那到底以誰 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()
高階函數
-
函數可以作為參數
-
函數可以作為返回值
作為參數
function eat (callback) {
?setTimeout(function () {
? ?console.log('吃完了')
? ?callback()
}, 1000)
}
?
eat(function () {
?console.log('去唱歌')
})
作為返回值
function genFun (type) {
?return function (obj) {
? ?return Object.prototype.toString.call(obj) === type
}
}
?
var isArray = genFun('[object Array]')
var isObject = genFun('[object Object]')
?
console.log(isArray([])) // => true
console.log(isArray({})) // => true
函數閉包
function fn () {
?var count = 0
?return {
? ?getCount: function () {
? ? ?console.log(count)
? },
? ?setCount: function () {
? ? ?count++
? }
}
}
?
var fns = fn()
?
fns.getCount() // => 0
fns.setCount()
fns.getCount() // => 1
作用域、作用域鏈、預解析
-
全局作用域
-
函數作用域
-
沒有塊級作用域
{
?var foo = 'bar'
}
?
console.log(foo)
?
if (true) {
?var a = 123
}
console.log(a)
作用域鏈示例代碼:
var a = 10
?
function fn () {
?var b = 20
?
?function fn1 () {
? ?var c = 30
? ?console.log(a + b + c)
}
?
?function fn2 () {
? ?var d = 40
? ?console.log(c + d)
}
?
?fn1()
?fn2()
}
-
內層作用域可以訪問外層作用域,反之不行
什么是閉包
閉包就是能夠讀取其他函數內部變量的函數,由于在 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)
示例3:投票
示例4:判斷類型
示例5:沙箱模式
閉包的思考題
思考題 1:
var name = "The Window";
var object = {
?name: "My Object",
?getNameFunc: function () {
? ?return function () {
? ? ?return this.name;
? };
}
};
?
console.log(object.getNameFunc()())
思考題 2:
var name = "The Window";
var object = {
?name: "My Object",
?getNameFunc: function () {
? ?var that = this;
? ?return function () {
? ? ?return that.name;
? };
}
};
console.log(object.getNameFunc()())
小結
函數遞歸
遞歸執行模型
function fn1 () {
?console.log(111)
?fn2()
?console.log('fn1')
}
?
function fn2 () {
?console.log(222)
?fn3()
?console.log('fn2')
}
?
function fn3 () {
?console.log(333)
?fn4()
?console.log('fn3')
}
?
function fn4 () {
?console.log(444)
?console.log('fn4')
}
?
fn1()
舉個栗子:計算階乘的遞歸函數
function factorial (num) {
?if (num <= 1) {
? ?return 1
} else {
? ?return num * factorial(num - 1)
}
}
遞歸應用場景
-
深拷貝
-
菜單樹
-
遍歷 DOM 樹
正則表達式
-
了解正則表達式基本語法
-
能夠使用JavaScript的正則對象
正則表達式簡介
什么是正則表達式
正則表達式:用于匹配規律規則的表達式,正則表達式最初是科學家對人類神經系統的工作原理的早期研究,現在在編程語言中有廣泛的應用。正則表通常被用來檢索、替換那些符合某個模式(規則)的文本。正則表達式是對字符串操作的一種邏輯公式,就是用事先定義好的一些特定字符、及這些特定字符的組合,組成一個“規則字符串”,這個“規則字符串”用來表達對字符串的一種過濾邏輯。
正則表達式的作用
-
給定的字符串是否符合正則表達式的過濾邏輯(匹配)
-
可以通過正則表達式,從字符串中獲取我們想要的特定部分(提取)
-
強大的字符串替換能力(替換)
正則表達式的特點
-
靈活性、邏輯性和功能性非常的強
-
可以迅速地用極簡單的方式達到字符串的復雜控制
-
對于剛接觸的人來說,比較晦澀難懂
正則表達式的測試
-
在線測試正則
-
工具中使用正則表達式
-
sublime/vscode/word
-
演示替換所有的數字
-
正則表達式的組成
-
普通字符
-
特殊字符(元字符):正則表達式中有特殊意義的字符
示例演示:
-
\d
匹配數字 -
ab\d
匹配 ab1、ab2
元字符串
通過測試工具演示下面元字符的使用
常用元字符串
元字符 | 說明 |
---|---|
\d | 匹配數字 |
\D | 匹配任意非數字的字符 |
\w | 匹配字母或數字或下劃線 |
\W | 匹配任意不是字母,數字,下劃線 |
\s | 匹配任意的空白符 |
\S | 匹配任意不是空白符的字符 |
. | 匹配除換行符以外的任意單個字符 |
^ | 表示匹配行首的文本(以誰開始) |
$ | 表示匹配行尾的文本(以誰結束) |
限定符
限定符 | 說明 |
---|---|
* | 重復零次或更多次 |
+ | 重復一次或更多次 |
? | 重復零次或一次 |
{n} | 重復n次 |
{n,} | 重復n次或更多次 |
{n,m} | 重復n到m次 |
其它
[] 字符串用中括號括起來,表示匹配其中的任一字符,相當于或的意思 [^] 匹配除中括號以內的內容 \ 轉義符 | 或者,選擇兩者中的一個。注意|將左右兩邊分為兩部分,而不管左右兩邊有多長多亂 () 從兩個直接量中選擇一個,分組eg:gr(a|e)y匹配gray和grey [\u4e00-\u9fa5] 匹配漢字
案例
驗證手機號:
^\d{11}$
驗證郵編:
^\d{6}$
驗證日期 2012-5-01
^\d{4}-\d{1,2}-\d{1,2}$
驗證郵箱 xxx@itcast.cn:
^\w+@\w+\.\w+$
驗證IP地址 192.168.1.10
^\d{1,3}\(.\d{1,3}){3}$
JavaScript 中使用正則表達式
創建正則對象
方式1:
var reg = new Regex('\d', 'i');
var reg = new Regex('\d', 'gi');
方式2:
var reg = /\d/i;
var reg = /\d/gi;
參數
標志 | 說明 |
---|---|
i | 忽略大小寫 |
g | 全局匹配 |
gi | 全局匹配+忽略大小寫 |
正則匹配
// 匹配日期
var dateStr = '2015-10-10';
var reg = /^\d{4}-\d{1,2}-\d{1,2}$/
console.log(reg.test(dateStr));
匹配正則表達式
// console.log(/./.test("除了回車換行以為的任意字符"));//true// console.log(/.*/.test("0個到多個"));//true// console.log(/.+/.test("1個到多個"));//true// console.log(/.?/.test("哈哈"));//true// console.log(/[0-9]/.test("9527"));//true// console.log(/[a-z]/.test("what"));//true// console.log(/[A-Z]/.test("Are"));//true// console.log(/[a-zA-Z]/.test("干啥子"));//false// console.log(/[0-9a-zA-Z]/.test("9ebg"));//true// console.log(/b|(ara)/.test("abra"));//true// console.log(/[a-z]{2,3}/.test("arfsf"));//true
console.log(/\d/.test("998"));//trueconsole.log(/\d*/.test("998"));//trueconsole.log(/\d+/.test("998"));//trueconsole.log(/\d{0,}/.test("998"));//trueconsole.log(/\d{2,3}/.test("998"));//trueconsole.log(/\D/.test("eat"));//trueconsole.log(/\s/.test(" "));//trueconsole.log(/\S/.test("嘎嘎"));//trueconsole.log(/\w/.test("_"));//trueconsole.log(/\W/.test("_"));//true
正則表達式案例
1.驗證密碼強弱2.驗證郵箱:[0-9a-zA-Z.-]+[@][0-9a-zA-Z.-]+(.+){1,2}3.驗證中文名字[\u4e00-\u9fa5]
正則提取
// 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-10
var 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;
}
補充
偽數組和數組
在JavaScript中,除了5種原始數據類型之外,其他所有的都是對象,包括函數(Function)。
對象與數組的關系
在說區別之前,需要先提到另外一個知識,就是 JavaScript 的原型繼承。所有 JavaScript 的內置構造函數都是繼承自 Object.prototype
。在這個前提下,可以理解為使用 new Array()
或 []
創建出來的數組對象,都會擁有 Object.prototype
的屬性值。
var obj = {};// 擁有 Object.prototype 的屬性值
var arr = [];
//使用數組直接量創建的數組,由于 Array.prototype 的屬性繼承自 Object.prototype,
//那么,它將同時擁有 Array.prototype 和 Object.prototype 的屬性值
可以得到對象和數組的第一個區別:對象沒有數組 Array.prototype 的屬性值。
什么是數組
數組具有一個最基本特征:索引,這是對象所沒有的,下面來看一段代碼:
var obj = {};
var arr = [];
obj[2] = 'a';
arr[2] = 'a';
console.log(obj[2]); // => a
console.log(arr[2]); // => a
console.log(obj.length); // => undefined
console.log(arr.length); // => 3
-
obj[2]輸出'a',是因為對象就是普通的鍵值對存取數據
-
而arr[2]輸出'a' 則不同,數組是通過索引來存取數據,arr[2]之所以輸出'a',是因為數組arr索引2的位置已經存儲了數據
-
obj.length并不具有數組的特性,并且obj沒有保存屬性length,那么自然就會輸出undefined
-
而對于數組來說,length是數組的一個內置屬性,數組會根據索引長度來更改length的值
-
為什么arr.length輸出3,而不是1
-
在給數組添加元素時,并沒有按照連續的索引添加,所以導致數組的索引不連續,那么就導致索引長度大于元素個數
-
什么是偽數組
-
擁有 length 屬性,其它屬性(索引)為非負整數(對象中的索引會被當做字符串來處理,這里你可以當做是個非負整數串來理解)
-
不具有數組所具有的方法
偽數組,就是像數組一樣有 length
屬性,也有 0、1、2、3
等屬性的對象,看起來就像數組一樣,但不是數組,比如:
var fakeArray = {
?"0": "first",
?"1": "second",
?"2": "third",
?length: 3
};
for (var i = 0; i < fakeArray.length; i++) {
?console.log(fakeArray[i]);
}
Array.prototype.join.call(fakeArray,'+');
常見的偽數組有:
-
函數內部的
arguments
-
DOM 對象列表(比如通過
document.getElementsByTags
得到的列表) -
jQuery 對象(比如
$("div")
)
偽數組是一個 Object,而真實的數組是一個 Array。
偽數組存在的意義,是可以讓普通的對象也能正常使用數組的很多方法,比如:
var arr = Array.prototype.slice.call(arguments);
Array.prototype.forEach.call(arguments, function(v) {
?// 循環arguments對象
});
?
// push
// some
// every
// filter
// map
// ...
以上在借用數組的原型方法的時候都可以通過數組直接量來簡化使用:
var obj = {
?0: 'a',
?1: 'b',
?2: 'c',
?length: 3
}
?
;[].push.call(obj, 'd')
?
console.log([].slice.call(obj))
?
;[].forEach.call(obj, function (num, index) {
?console.log(num)
})
小結
-
對象沒有數組 Array.prototype 的屬性值,類型是 Object ,而數組類型是 Array
-
數組是基于索引的實現, length 會自動更新,而對象是鍵值對
-
使用對象可以創建偽數組,偽數組可以正常使用數組的大部分方法
JavaScript 垃圾回收機制
JavaScript 運行機制:Event Loop
Object
靜態成員
-
Object.assign()
-
Object.create()
-
Object.keys()
-
Object.defineProperty()
實例成員
-
constructor
-
hasOwnProperty()
-
isPrototypeOf
-
propertyIsEnumerable()
-
toString()
-
valueOf()
附錄
A 代碼規范
代碼風格
-
JavaScript Standard Style
-
Airbnb JavaScript Style Guide() {
校驗工具
-
JSLint
-
JSHint
-
ESLint
B Chrome 開發者工具
C 文檔相關工具
-
電子文檔制作工具: docute
-
流程圖工具:
2019-08-0318:11:32
作者:何秀好