js(十)——高級

學習目標: - 理解面向對象開發思想 
- 掌握 JavaScript 面向對象開發相關模式 
- 掌握在 JavaScript 中使用正則表達式 

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 與 Ecmascript 的關係

  • JavaScript 與 Java 的關係

  • JavaScript 的版本

    • 2015年6月,ECMAScript 6 正式發佈,並且更名爲“ECMAScript 2015”。這是因爲 TC39 委員會計劃,以後每年發佈一個 ECMAScript 的版本,下一個版本在2016年發佈,稱爲“ECMAScript 2016”,2017年發佈“ECMAScript 2017”,以此類推。

  • 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 把對象定義爲:無序屬性的集合,其屬性可以包含基本值、對象或者函數。 嚴格來講,這就相當於說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都 映射到一個值。

提示:每個對象都是基於一個引用類型創建的,這些類型可以是系統內置的原生類型,也可以是開發人員自定義的類型。

什麼是面向對象

面向對象不是新的東西,它只是過程式代碼的一種高度封裝,目的在於提高代碼的開發效率和可維護性。

面向對象編程 —— 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 這種數據類型應該被視爲一個對象,這個對象擁有 namescore 這兩個屬性(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) {
      var obj=new Object();
      obj.name=name;
      obj.age=age;
      obj.sayHi=function () {
        console.log("您好");
      };
      return obj;
}

然後生成實例對象:

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 個步驟:

  1. 創建一個新對象

  2. 將構造函數的作用域賦給新對象(因此 this 就指向了這個新對象)

  3. 執行構造函數中的代碼

  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

總結:

  • 構造函數是根據具體的事物抽象出來的抽象模板

  • 實例對象是根據抽象的構造函數模板得到的具體實例對象

  • 每一個實例對象都具有一個 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)

在該示例中,從表面上好像沒什麼問題,但是實際上這樣做,有一個很大的弊端。 那就是對於每一個實例對象,typesayHello 都是一模一樣的內容, 每一次生成一個實例,都必須爲重複的內容,多佔用一些內存,如果實例對象很多,會造成極大的內存浪費。

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 原型對象解決構造函數的問題

  • 分析 構造函數、prototype 原型對象、實例對象 三者之間的關係

  • 屬性成員搜索原則:原型鏈

  • 實例對象讀寫原型對象中的成員

  • 原型對象的簡寫形式

  • 原生對象的原型

    • Object

    • Array

    • String

    • ...

  • 原型對象的問題

  • 構造的函數和原型對象使用建議

更好的解決方案: prototype

Javascript 規定,每一個構造函數都有一個 prototype 屬性,指向另一個對象。 這個對象的所有屬性和方法,都會被構造函數的實例繼承。

這也就意味着,我們可以把所有對象實例需要共享的屬性和方法直接定義在 prototype 對象上。

function Person (name, age) {
  this.name = name
  this.age = age
}
​
console.log(Person.prototype)
​
//通過構造函數的原型對象創建一個sayName屬性
Person.prototype.type = 'human'
​
//通過構造函數的原型對象創建一個sayName方法
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

‘prototype ’是標準原型屬性,程序員專用

`__proto__`  是非標準屬性,瀏覽器專用

實例對象可以直接訪問原型對象成員。

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 + '歲了')
  }
}

原生對象的原型

所有函數都有 prototype 屬性對象。

  • Object.prototype

  • Function.prototype

  • Array.prototype

  • String.prototype

  • Number.prototype

  • Date.prototype

  • ...

練習:爲數組對象和字符串對象擴展原型方法。比如給內置對象String添加一個字符串倒置的方法

原型對象的問題

  • 共享數組

  • 共享對象

如果真的希望可以被實例對象之間共享和修改這些共享數據那就不是問題。但是如果不希望實例之間共享和修改這些共享數據則就是問題。

一個更好的建議是,最好不要讓實例之間互相共享這些數組或者對象成員,一旦修改的話會導致數據的走向很不明確而且難以維護。

原型對象使用建議

  • 私有成員(一般就是非函數成員)放到構造函數中

  • 共享成員(一般就是函數)放到原型對象中

  • 如果重置了 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方法

  1. 讓蛇移動起來,把蛇身體的每一部分往前移動一下

  2. 蛇頭部分根據不同的方向決定 往哪裏移動

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 值和分別地提供的參數(參數的列表)。

 

​注意:該方法的作用和 `apply()` 方法類似,只有一個區別,就是 `call()` 方法接受的是若干個參數的列表,而 `apply()` 方法接受的是一個包含多個參數的數組。

語法:

fun.call(thisArg[, arg1[, arg2[, ...]]])

參數:

  • thisArg

    • 在 fun 函數運行時指定的 this 值

    • 如果指定了 null 或者 undefined 則內部 this 指向 window

  • arg1, arg2, ...

    • 指定的參數列表

apply

apply() 方法調用一個函數, 其具有一個指定的 this 值,以及作爲一個數組(或類似數組的對象)提供的參數。

注意:該方法的作用和 `call()` 方法類似,只有一個區別,就是 `call()` 方法接受的是若干個參數的列表,而 `apply()` 方法接受的是一個包含多個參數的數組。

語法:

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 支持傳遞參數,它的傳參方式比較特殊,一共有兩個位置可以傳遞

        1. 在 bind 的同時,以參數列表的形式進行傳遞

        1. 在調用的時候,以參數列表的形式進行傳遞

      • 那到底以誰 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的正則對象

正則表達式簡介

什麼是正則表達式

正則表達式:用於匹配規律規則的表達式,正則表達式最初是科學家對人類神經系統的工作原理的早期研究,現在在編程語言中有廣泛的應用。正則表通常被用來檢索、替換那些符合某個模式(規則)的文本。 正則表達式是對字符串操作的一種邏輯公式,就是用事先定義好的一些特定字符、及這些特定字符的組合,組成一個“規則字符串”,這個“規則字符串”用來表達對字符串的一種過濾邏輯。

正則表達式的作用

  1. 給定的字符串是否符合正則表達式的過濾邏輯(匹配)

  2. 可以通過正則表達式,從字符串中獲取我們想要的特定部分(提取)

  3. 強大的字符串替換能力(替換)

正則表達式的特點

  1. 靈活性、邏輯性和功能性非常的強

  2. 可以迅速地用極簡單的方式達到字符串的複雜控制

  3. 對於剛接觸的人來說,比較晦澀難懂

正則表達式的測試

  • 在線測試正則

  • 工具中使用正則表達式

    • 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}$

驗證郵箱 [email protected]

^\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"));//true
    console.log(/\d*/.test("998"));//true
    console.log(/\d+/.test("998"));//true
    console.log(/\d{0,}/.test("998"));//true
    console.log(/\d{2,3}/.test("998"));//true
    console.log(/\D/.test("eat"));//true
    console.log(/\s/.test("  "));//true
    console.log(/\S/.test("嘎嘎"));//true
    console.log(/\w/.test("_"));//true
    console.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 = "[email protected],[email protected] [email protected] 2、[email protected] [email protected]...";
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 = "[email protected]";
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

    • 在給數組添加元素時,並沒有按照連續的索引添加,所以導致數組的索引不連續,那麼就導致索引長度大於元素個數

什麼是僞數組

  1. 擁有 length 屬性,其它屬性(索引)爲非負整數(對象中的索引會被當做字符串來處理,這裏你可以當做是個非負整數串來理解)

  2. 不具有數組所具有的方法

僞數組,就是像數組一樣有 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 代碼規範

代碼風格

校驗工具

B Chrome 開發者工具

C 文檔相關工具

發佈了108 篇原創文章 · 獲贊 31 · 訪問量 9305
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章