03-JavaScript高級

JavaScript 的組成

  • ECMAScript - 語法規範
    • 變量、數據類型、類型轉換、操作符
    • 流程控制語句:判斷、循環語句
    • 數組、函數、作用域、預解析
    • 對象、屬性、方法、簡單類型和複雜類型的區別
    • 內置對象:Math、Date、Array,基本包裝類型String、Number、Boolean
  • Web APIs
    • BOM
      • onload頁面加載事件,window頂級對象
      • 定時器
      • location、history
    • DOM
      • 獲取頁面元素,註冊事件
      • 屬性操作,樣式操作
      • 節點屬性,節點層級
      • 動態創建元素
      • 事件:註冊事件的方式、事件的三個階段、事件對象

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

  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
}

構造函數和實例對象的關係

使用構造函數的好處不僅僅在於代碼的簡潔性,更重要的是我們可以識別對象的具體類型了。
在每一個實例對象中同時有一個 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('Tom', 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('Top', 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 原型對象、實例對象 三者之間的關係
  • 屬性成員搜索原則:原型鏈
  • 實例對象讀寫原型對象中的成員
  • 原型對象的簡寫形式
  • 原生對象的原型
    • 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(...)
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.prototype.constructor === F) // => true

通過構造函數得到的實例對象內部會包含一個指向構造函數的 prototype 對象的指針 __proto__

var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true

`__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

原型對象使用建議

  • 私有成員(一般就是非函數成員)放到構造函數中
  • 共享成員(一般就是函數)放到原型對象中
  • 如果重置了 prototype 記得修正 constructor 的指向

繼承

什麼是繼承

  • 現實生活中的繼承
  • 程序中的繼承

構造函數的屬性繼承:借用構造函數

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)

閉包的思考題

思考題 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));

正則提取

// 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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章