JavaScript高級


學(xué)習(xí)目標(biāo):

  • 理解面向?qū)ο箝_發(fā)思想
  • 掌握 JavaScript 面向?qū)ο箝_發(fā)相關(guān)模式
  • 掌握在 JavaScript 中使用正則表達(dá)式

typora-copy-images-to media

JavaScript 高級

<img src="./media/Unofficial_JavaScript_logo_2.svg.png" width="400" alt="">

目標(biāo)

  • 理解面向?qū)ο箝_發(fā)思想
  • 掌握 JavaScript 面向?qū)ο箝_發(fā)相關(guān)模式
  • 掌握在 JavaScript 中使用正則表達(dá)式

案例演示


回顧

由于 JavaScript 高級還是針對 JavaScript 語言本身的一個(gè)進(jìn)階學(xué)習(xí)赞别,所以在開始之前我們先對以前所學(xué)過的 JavaScript 相關(guān)知識點(diǎn)做一個(gè)快速復(fù)習(xí)總結(jié)杜顺。

重新介紹 JavaScript

JavaScript 是什么

  • 解析執(zhí)行:輕量級解釋型的

  • 語言特點(diǎn):動(dòng)態(tài)疹娶,頭等函數(shù) (First-class Function)

    • 又稱函數(shù)是 JavaScript 中的一等公民
  • 執(zhí)行環(huán)境:在宿主環(huán)境(host environment)下運(yùn)行戳晌,瀏覽器是最常見的 JavaScript 宿主環(huán)境

    • 但是在很多非瀏覽器環(huán)境中也使用 JavaScript 叭爱,例如 node.js

    MDN-JavaScript

JavaScript 的組成

  • ECMAScript - 語法規(guī)范
    • 變量撞叨、數(shù)據(jù)類型愿吹、類型轉(zhuǎn)換不从、操作符
    • 流程控制語句:判斷、循環(huán)語句
    • 數(shù)組犁跪、函數(shù)椿息、作用域歹袁、預(yù)解析
    • 對象、屬性寝优、方法条舔、簡單類型和復(fù)雜類型的區(qū)別
    • 內(nèi)置對象:Math、Date乏矾、Array孟抗,基本包裝類型String、Number钻心、Boolean
  • Web APIs
    • BOM
      • onload頁面加載事件凄硼,window頂級對象
      • 定時(shí)器
      • location、history
    • DOM
      • 獲取頁面元素捷沸,注冊事件
      • 屬性操作摊沉,樣式操作
      • 節(jié)點(diǎn)屬性,節(jié)點(diǎn)層級
      • 動(dòng)態(tài)創(chuàng)建元素
      • 事件:注冊事件的方式痒给、事件的三個(gè)階段说墨、事件對象

JavaScript 可以做什么

阿特伍德定律:

Any application that can be written in JavaScript, will eventually be written in JavaScript.

任何可以用JavaScript來寫的應(yīng)用,最終都將用JavaScript來寫

阿特伍德 stackoverflow的創(chuàng)始人之一

瀏覽器是如何工作的

參考鏈接

User Interface  用戶界面试吁,我們所看到的瀏覽器
Browser engine  瀏覽器引擎棺棵,用來查詢和操作渲染引擎
*Rendering engine 用來顯示請求的內(nèi)容,負(fù)責(zé)解析HTML潘悼、CSS律秃,并把解析的內(nèi)容顯示出來
Networking   網(wǎng)絡(luò)爬橡,負(fù)責(zé)發(fā)送網(wǎng)絡(luò)請求
*JavaScript Interpreter(解析者)   JavaScript解析器治唤,負(fù)責(zé)執(zhí)行JavaScript的代碼
UI Backend   UI后端,用來繪制類似組合框和彈出窗口
Data Persistence(持久化)  數(shù)據(jù)持久化糙申,數(shù)據(jù)存儲  cookie宾添、HTML5中的sessionStorage

JavaScript 執(zhí)行過程

JavaScript 運(yùn)行分為兩個(gè)階段:

  • 預(yù)解析
    • 全局預(yù)解析(所有變量和函數(shù)聲明都會提前;同名的函數(shù)和變量函數(shù)的優(yōu)先級高)
    • 函數(shù)內(nèi)部預(yù)解析(所有的變量柜裸、函數(shù)和形參都會參與預(yù)解析)
      • 函數(shù)
      • 形參
      • 普通變量
  • 執(zhí)行

先預(yù)解析全局作用域缕陕,然后執(zhí)行全局作用域中的代碼,
在執(zhí)行全局代碼的過程中遇到函數(shù)調(diào)用就會先進(jìn)行函數(shù)預(yù)解析疙挺,然后再執(zhí)行函數(shù)內(nèi)代碼扛邑。


JavaScript 面向?qū)ο缶幊?/h2>

<img src="./media/mxdxkf.png" width="400" alt="">

面向?qū)ο蠼榻B

什么是對象

Everything is object (萬物皆對象)

<img src="./media/20160823024542444.jpg" alt="">

對象到底是什么,我們可以從兩次層次來理解铐然。

(1) 對象是單個(gè)事物的抽象蔬崩。

一本書恶座、一輛汽車、一個(gè)人都可以是對象沥阳,一個(gè)數(shù)據(jù)庫跨琳、一張網(wǎng)頁、一個(gè)與遠(yuǎn)程服務(wù)器的連接也可以是對象桐罕。當(dāng)實(shí)物被抽象成對象脉让,實(shí)物之間的關(guān)系就變成了對象之間的關(guān)系,從而就可以模擬現(xiàn)實(shí)情況功炮,針對對象進(jìn)行編程溅潜。

(2) 對象是一個(gè)容器,封裝了屬性(property)和方法(method)薪伏。

屬性是對象的狀態(tài)伟恶,方法是對象的行為(完成某種任務(wù))。比如毅该,我們可以把動(dòng)物抽象為animal對象博秫,使用“屬性”記錄具體是那一種動(dòng)物,使用“方法”表示動(dòng)物的某種行為(奔跑眶掌、捕獵挡育、休息等等)。

在實(shí)際開發(fā)中朴爬,對象是一個(gè)抽象的概念即寒,可以將其簡單理解為:數(shù)據(jù)集或功能集

ECMAScript-262 把對象定義為:無序?qū)傩缘募险儇鋵傩钥梢园局的刚浴ο蠡蛘吆瘮?shù)
嚴(yán)格來講具滴,這就相當(dāng)于說對象是一組沒有特定順序的值凹嘲。對象的每個(gè)屬性或方法都有一個(gè)名字,而每個(gè)名字都映射到一個(gè)值构韵。

提示:每個(gè)對象都是基于一個(gè)引用類型創(chuàng)建的周蹭,這些類型可以是系統(tǒng)內(nèi)置的原生類型,也可以是開發(fā)人員自定義的類型疲恢。

什么是面向?qū)ο?/h4>

面向?qū)ο蟛皇切碌臇|西凶朗,它只是過程式代碼的一種高度封裝,目的在于提高代碼的開發(fā)效率和可維 護(hù)性显拳。

<img src="./media/664ba37eeee9f4623c06c066867f1d38_r.jpg" width="400" alt="">

面向?qū)ο缶幊?—— Object Oriented Programming棚愤,簡稱 OOP ,是一種編程開發(fā)思想杂数。
它將真實(shí)世界各種復(fù)雜的關(guān)系宛畦,抽象為一個(gè)個(gè)對象矛绘,然后由對象之間的分工與合作,完成對真實(shí)世界的模擬刃永。

在面向?qū)ο蟪绦蜷_發(fā)思想中货矮,每一個(gè)對象都是功能中心,具有明確分工斯够,可以完成接受信息囚玫、處理數(shù)據(jù)、發(fā)出信息等任務(wù)读规。
因此抓督,面向?qū)ο缶幊叹哂徐`活、代碼可復(fù)用束亏、高度模塊化等特點(diǎn)铃在,容易維護(hù)和開發(fā),比起由一系列函數(shù)或指令組成的傳統(tǒng)的過程式編程(procedural programming)碍遍,更適合多人合作的大型軟件項(xiàng)目定铜。

面向?qū)ο笈c面向過程:

  • 面向過程就是親力親為,事無巨細(xì)怕敬,面面俱到揣炕,步步緊跟,有條不紊
  • 面向?qū)ο缶褪钦乙粋€(gè)對象东跪,指揮得結(jié)果
  • 面向?qū)ο髮?zhí)行者轉(zhuǎn)變成指揮者
  • 面向?qū)ο蟛皇敲嫦蜻^程的替代畸陡,而是面向過程的封裝

面向?qū)ο蟮奶匦裕?/p>

  • 封裝性
  • 繼承性
  • [多態(tài)性]抽象

擴(kuò)展閱讀:

程序中面向?qū)ο蟮幕倔w現(xiàn)

在 JavaScript 中,所有數(shù)據(jù)類型都可以視為對象虽填,當(dāng)然也可以自定義對象丁恭。
自定義的對象數(shù)據(jù)類型就是面向?qū)ο笾械念悾?Class )的概念。

我們以一個(gè)例子來說明面向過程和面向?qū)ο笤诔绦蛄鞒躺系牟煌帯?/p>

假設(shè)我們要處理學(xué)生的成績表斋日,為了表示一個(gè)學(xué)生的成績牲览,面向過程的程序可以用一個(gè)對象表示:

var std1 = { name: 'Michael', score: 98 }
var std2 = { name: 'Bob', score: 81 }

而處理學(xué)生成績可以通過函數(shù)實(shí)現(xiàn),比如打印學(xué)生的成績:

function printScore (student) {
  console.log('姓名:' + student.name + '  ' + '成績:' + student.score)
}

如果采用面向?qū)ο蟮某绦蛟O(shè)計(jì)思想桑驱,我們首選思考的不是程序的執(zhí)行流程竭恬,
而是 Student 這種數(shù)據(jù)類型應(yīng)該被視為一個(gè)對象,這個(gè)對象擁有 namescore 這兩個(gè)屬性(Property)熬的。
如果要打印一個(gè)學(xué)生的成績,首先必須創(chuàng)建出這個(gè)學(xué)生對應(yīng)的對象赊级,然后押框,給對象發(fā)一個(gè) printScore 消息,讓對象自己把自己的數(shù)據(jù)打印出來理逊。

抽象數(shù)據(jù)行為模板(Class):

function Student(name, score) {
  this.name = name;
  this.score = score;
  this.printScore = function() {
    console.log('姓名:' + this.name + '  ' + '成績:' + this.score);
  }
}

根據(jù)模板創(chuàng)建具體實(shí)例對象(Instance):

var std1 = new Student('Michael', 98)
var std2 = new Student('Bob', 81)

實(shí)例對象具有自己的具體行為(給對象發(fā)消息):

std1.printScore() // => 姓名:Michael  成績:98
std2.printScore() // => 姓名:Bob  成績 81

面向?qū)ο蟮脑O(shè)計(jì)思想是從自然界中來的橡伞,因?yàn)樵谧匀唤缰泻腥啵悾–lass)和實(shí)例(Instance)的概念是很自然的。
Class 是一種抽象概念兑徘,比如我們定義的 Class——Student 刚盈,是指學(xué)生這個(gè)概念,
而實(shí)例(Instance)則是一個(gè)個(gè)具體的 Student 挂脑,比如藕漱, Michael 和 Bob 是兩個(gè)具體的 Student 。

所以崭闲,面向?qū)ο蟮脑O(shè)計(jì)思想是:

  • 抽象出 Class(構(gòu)造函數(shù))
  • 根據(jù) Class(構(gòu)造函數(shù)) 創(chuàng)建 Instance
  • 指揮 Instance 得結(jié)果

面向?qū)ο蟮某橄蟪潭扔直群瘮?shù)要高肋联,因?yàn)橐粋€(gè) Class 既包含數(shù)據(jù),又包含操作數(shù)據(jù)的方法刁俭。

創(chuàng)建對象

簡單方式

我們可以直接通過 new Object() 創(chuàng)建:

var person = new Object()
person.name = 'Jack'
person.age = 18

person.sayName = function () {
  console.log(this.name)
}

每次創(chuàng)建通過 new Object() 比較麻煩橄仍,所以可以通過它的簡寫形式對象字面量來創(chuàng)建:

var person = {
  name: 'Jack',
  age: 18,
  sayName: function () {
    console.log(this.name)
  }
}

對于上面的寫法固然沒有問題,但是假如我們要生成兩個(gè) person 實(shí)例對象呢牍戚?

var person1 = {
  name: 'Jack',
  age: 18,
  sayName: function () {
    console.log(this.name)
  }
}

var person2 = {
  name: 'Mike',
  age: 16,
  sayName: function () {
    console.log(this.name)
  }
}

通過上面的代碼我們不難看出侮繁,這樣寫的代碼太過冗余,重復(fù)性太高如孝。

簡單方式的改進(jìn):工廠函數(shù)

我們可以寫一個(gè)函數(shù)鼎天,解決代碼重復(fù)問題:

function createPerson (name, age) {
  return {
    name: name,
    age: age,
    sayName: function () {
      console.log(this.name)
    }
  }
}

然后生成實(shí)例對象:

var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)

這樣封裝確實(shí)爽多了,通過工廠模式我們解決了創(chuàng)建多個(gè)相似對象代碼冗余的問題暑竟,
但卻沒有解決對象識別的問題(即怎樣知道一個(gè)對象的類型)斋射。

構(gòu)造函數(shù)

內(nèi)容引導(dǎo):

  • 構(gòu)造函數(shù)語法
  • 分析構(gòu)造函數(shù)
  • 構(gòu)造函數(shù)和實(shí)例對象的關(guān)系
    • 實(shí)例的 constructor 屬性
    • instanceof 操作符
  • 普通函數(shù)調(diào)用和構(gòu)造函數(shù)調(diào)用的區(qū)別
  • 構(gòu)造函數(shù)的返回值
  • 構(gòu)造函數(shù)的問題

更優(yōu)雅的工廠函數(shù):構(gòu)造函數(shù)

一種更優(yōu)雅的工廠函數(shù)就是下面這樣,構(gòu)造函數(shù):

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

解析構(gòu)造函數(shù)代碼的執(zhí)行

在上面的示例中但荤,Person() 函數(shù)取代了 createPerson() 函數(shù)罗岖,但是實(shí)現(xiàn)效果是一樣的。
這是為什么呢腹躁?

我們注意到桑包,Person() 中的代碼與 createPerson() 有以下幾點(diǎn)不同之處:

  • 沒有顯示的創(chuàng)建對象
  • 直接將屬性和方法賦給了 this 對象
  • 沒有 return 語句
  • 函數(shù)名使用的是大寫的 Person

而要?jiǎng)?chuàng)建 Person 實(shí)例,則必須使用 new 操作符纺非。
以這種方式調(diào)用構(gòu)造函數(shù)會經(jīng)歷以下 4 個(gè)步驟:

  1. 創(chuàng)建一個(gè)新對象
  2. 將構(gòu)造函數(shù)的作用域賦給新對象(因此 this 就指向了這個(gè)新對象)
  3. 執(zhí)行構(gòu)造函數(shù)中的代碼
  4. 返回新對象

下面是具體的偽代碼:

function Person (name, age) {
  // 當(dāng)使用 new 操作符調(diào)用 Person() 的時(shí)候哑了,實(shí)際上這里會先創(chuàng)建一個(gè)對象
  // var instance = {}
  // 然后讓內(nèi)部的 this 指向 instance 對象
  // this = instance
  // 接下來所有針對 this 的操作實(shí)際上操作的就是 instance

  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }

  // 在函數(shù)的結(jié)尾處會將 this 返回,也就是 instance
  // return this
}

構(gòu)造函數(shù)和實(shí)例對象的關(guān)系

使用構(gòu)造函數(shù)的好處不僅僅在于代碼的簡潔性,更重要的是我們可以識別對象的具體類型了谦纱。
在每一個(gè)實(shí)例對象中同時(shí)有一個(gè) constructor 屬性菱农,該屬性指向創(chuàng)建該實(shí)例的構(gòu)造函數(shù):

console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true

對象的 constructor 屬性最初是用來標(biāo)識對象類型的,
但是拆火,如果要檢測對象的類型,還是使用 instanceof 操作符更可靠一些:

console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true

總結(jié):

  • 構(gòu)造函數(shù)是根據(jù)具體的事物抽象出來的抽象模板
  • 實(shí)例對象是根據(jù)抽象的構(gòu)造函數(shù)模板得到的具體實(shí)例對象
  • 每一個(gè)實(shí)例對象都具有一個(gè) constructor 屬性,指向創(chuàng)建該實(shí)例的構(gòu)造函數(shù)
    • 注意: constructor 是實(shí)例的屬性的說法不嚴(yán)謹(jǐn)们镜,具體后面的原型會講到
  • 可以通過實(shí)例的 constructor 屬性判斷實(shí)例和構(gòu)造函數(shù)之間的關(guān)系
    • 注意:這種方式不嚴(yán)謹(jǐn)币叹,推薦使用 instanceof 操作符,后面學(xué)原型會解釋為什么

構(gòu)造函數(shù)的問題

使用構(gòu)造函數(shù)帶來的最大的好處就是創(chuàng)建對象更方便了模狭,但是其本身也存在一個(gè)浪費(fèi)內(nèi)存的問題:

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)

在該示例中颈抚,從表面上好像沒什么問題,但是實(shí)際上這樣做嚼鹉,有一個(gè)很大的弊端贩汉。
那就是對于每一個(gè)實(shí)例對象,typesayHello 都是一模一樣的內(nèi)容反砌,
每一次生成一個(gè)實(shí)例雾鬼,都必須為重復(fù)的內(nèi)容,多占用一些內(nèi)存宴树,如果實(shí)例對象很多策菜,會造成極大的內(nèi)存浪費(fèi)。

console.log(p1.sayHello === p2.sayHello) // => false

對于這種問題我們可以把需要共享的函數(shù)定義到構(gòu)造函數(shù)外部:

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

這樣確實(shí)可以了酒贬,但是如果有多個(gè)需要共享的函數(shù)的話就會造成全局命名空間沖突的問題又憨。

你肯定想到了可以把多個(gè)函數(shù)放到一個(gè)對象中用來避免全局命名空間沖突的問題:

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

至此,我們利用自己的方式基本上解決了構(gòu)造函數(shù)的內(nèi)存浪費(fèi)問題锭吨。
但是代碼看起來還是那么的格格不入蠢莺,那有沒有更好的方式呢?

小結(jié)

  • 構(gòu)造函數(shù)語法
  • 分析構(gòu)造函數(shù)
  • 構(gòu)造函數(shù)和實(shí)例對象的關(guān)系
    • 實(shí)例的 constructor 屬性
    • instanceof 操作符
  • 構(gòu)造函數(shù)的問題

原型

內(nèi)容引導(dǎo):

  • 使用 prototype 原型對象解決構(gòu)造函數(shù)的問題
  • 分析 構(gòu)造函數(shù)零如、prototype 原型對象躏将、實(shí)例對象 三者之間的關(guān)系
  • 屬性成員搜索原則:原型鏈
  • 實(shí)例對象讀寫原型對象中的成員
  • 原型對象的簡寫形式
  • 原生對象的原型
    • Object
    • Array
    • String
    • ...
  • 原型對象的問題
  • 構(gòu)造的函數(shù)和原型對象使用建議

更好的解決方案: prototype

JavaScript 規(guī)定,每一個(gè)構(gòu)造函數(shù)都有一個(gè) prototype 屬性考蕾,指向另一個(gè)對象祸憋。
這個(gè)對象的所有屬性和方法,都會被構(gòu)造函數(shù)的所擁有肖卧。

這也就意味著蚯窥,我們可以把所有對象實(shí)例需要共享的屬性和方法直接定義在 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

這時(shí)所有實(shí)例的 type 屬性和 sayName() 方法塞帐,
其實(shí)都是同一個(gè)內(nèi)存地址拦赠,指向 prototype 對象,因此就提高了運(yùn)行效率葵姥。

構(gòu)造函數(shù)荷鼠、實(shí)例、原型三者之間的關(guān)系

<img src="./media/構(gòu)造函數(shù)-實(shí)例-原型之間的關(guān)系.png" alt="">

任何函數(shù)都具有一個(gè) prototype 屬性牌里,該屬性是一個(gè)對象颊咬。

function F () {}
console.log(F.prototype) // => object

F.prototype.sayHi = function () {
  console.log('hi!')
}

構(gòu)造函數(shù)的 prototype 對象默認(rèn)都有一個(gè) constructor 屬性务甥,指向 prototype 對象所在函數(shù)牡辽。

console.log(F.prototype.constructor === F) // => true

通過構(gòu)造函數(shù)得到的實(shí)例對象內(nèi)部會包含一個(gè)指向構(gòu)造函數(shù)的 prototype 對象的指針 __proto__喳篇。

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

<p class="tip">
__proto__ 是非標(biāo)準(zhǔn)屬性。
</p>

實(shí)例對象可以直接訪問原型對象成員态辛。

instance.sayHi() // => hi!

總結(jié):

  • 任何函數(shù)都具有一個(gè) prototype 屬性麸澜,該屬性是一個(gè)對象
  • 構(gòu)造函數(shù)的 prototype 對象默認(rèn)都有一個(gè) constructor 屬性,指向 prototype 對象所在函數(shù)
  • 通過構(gòu)造函數(shù)得到的實(shí)例對象內(nèi)部會包含一個(gè)指向構(gòu)造函數(shù)的 prototype 對象的指針 __proto__
  • 所有實(shí)例都直接或間接繼承了原型對象的成員

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

了解了 構(gòu)造函數(shù)-實(shí)例-原型對象 三者之間的關(guān)系后奏黑,接下來我們來解釋一下為什么實(shí)例對象可以訪問原型對象中的成員炊邦。

每當(dāng)代碼讀取某個(gè)對象的某個(gè)屬性時(shí),都會執(zhí)行一次搜索熟史,目標(biāo)是具有給定名字的屬性

  • 搜索首先從對象實(shí)例本身開始
  • 如果在實(shí)例中找到了具有給定名字的屬性馁害,則返回該屬性的值
  • 如果沒有找到,則繼續(xù)搜索指針指向的原型對象蹂匹,在原型對象中查找具有給定名字的屬性
  • 如果在原型對象中找到了這個(gè)屬性碘菜,則返回該屬性的值

也就是說,在我們調(diào)用 person1.sayName() 的時(shí)候限寞,會先后執(zhí)行兩次搜索:

  • 首先忍啸,解析器會問:“實(shí)例 person1 有 sayName 屬性嗎?”答:“沒有履植。
  • ”然后计雌,它繼續(xù)搜索,再問:“ person1 的原型有 sayName 屬性嗎玫霎?”答:“有凿滤。
  • ”于是,它就讀取那個(gè)保存在原型對象中的函數(shù)庶近。
  • 當(dāng)我們調(diào)用 person2.sayName() 時(shí)翁脆,將會重現(xiàn)相同的搜索過程,得到相同的結(jié)果拦盹。

而這正是多個(gè)對象實(shí)例共享原型所保存的屬性和方法的基本原理鹃祖。

總結(jié):

  • 先在自己身上找,找到即返回
  • 自己身上找不到普舆,則沿著原型鏈向上查找恬口,找到即返回
  • 如果一直到原型鏈的末端還沒有找到,則返回 undefined

實(shí)例對象讀寫原型對象成員

讀日勇隆:

  • 先在自己身上找祖能,找到即返回
  • 自己身上找不到,則沿著原型鏈向上查找蛾洛,找到即返回
  • 如果一直到原型鏈的末端還沒有找到养铸,則返回 undefined

值類型成員寫入(實(shí)例對象.值類型成員 = xx):

  • 當(dāng)實(shí)例期望重寫原型對象中的某個(gè)普通數(shù)據(jù)成員時(shí)實(shí)際上會把該成員添加到自己身上
  • 也就是說該行為實(shí)際上會屏蔽掉對原型對象成員的訪問

引用類型成員寫入(實(shí)例對象.引用類型成員 = xx):

  • 同上

復(fù)雜類型修改(實(shí)例對象.成員.xx = xx):

  • 同樣會先在自己身上找該成員雁芙,如果自己身上找到則直接修改
  • 如果自己身上找不到,則沿著原型鏈繼續(xù)查找钞螟,如果找到則修改
  • 如果一直到原型鏈的末端還沒有找到該成員兔甘,則報(bào)錯(cuò)(實(shí)例對象.undefined.xx = xx

更簡單的原型語法

我們注意到,前面例子中每添加一個(gè)屬性和方法就要敲一遍 Person.prototype 鳞滨。
為減少不必要的輸入洞焙,更常見的做法是用一個(gè)包含所有屬性和方法的對象字面量來重寫整個(gè)原型對象:

function Person (name, age) {
  this.name = name
  this.age = age
}

Person.prototype = {
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '歲了')
  }
}

在該示例中拯啦,我們將 Person.prototype 重置到了一個(gè)新的對象澡匪。
這樣做的好處就是為 Person.prototype 添加成員簡單了,但是也會帶來一個(gè)問題褒链,那就是原型對象丟失了 constructor 成員唁情。

所以,我們?yōu)榱吮3?constructor 的指向正確甫匹,建議的寫法是:

function Person (name, age) {
  this.name = name
  this.age = age
}

Person.prototype = {
  constructor: Person, // => 手動(dòng)將 constructor 指向正確的構(gòu)造函數(shù)
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + '甸鸟,我今年' + this.age + '歲了')
  }
}

原生對象的原型

<p class="tip">
所有函數(shù)都有 prototype 屬性對象。
</p>

  • Object.prototype
  • Function.prototype
  • Array.prototype
  • String.prototype
  • Number.prototype
  • Date.prototype
  • ...

練習(xí):為數(shù)組對象和字符串對象擴(kuò)展原型方法赛惩。

原型對象使用建議

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

案例:隨機(jī)方塊


面向?qū)ο笥螒虬咐贺澇陨?/h2>

案例介紹

游戲演示

演示:貪吃蛇

案例目標(biāo)

游戲的目的是用來體會js高級語法的使用 不需要具備抽象對象的能力哀墓,使用面向?qū)ο蟮姆绞椒治鰡栴},需要一個(gè)漫長的過程喷兼。

功能實(shí)現(xiàn)

搭建頁面

放一個(gè)容器盛放游戲場景 div#map篮绰,設(shè)置樣式

#map {
  width: 800px;
  height: 600px;
  background-color: #ccc;
  position: relative;
}

分析對象

  • 游戲?qū)ο?/li>
  • 蛇對象
  • 食物對象

創(chuàng)建食物對象

  • Food

    • 屬性

      • x
      • y
      • width
      • height
      • color
    • 方法

      • render 隨機(jī)創(chuàng)建一個(gè)食物對象,并輸出到map上
  • 創(chuàng)建Food的構(gòu)造函數(shù)季惯,并設(shè)置屬性

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';
}
  • 通過原型設(shè)置render方法吠各,實(shí)現(xiàn)隨機(jī)產(chǎn)生食物對象,并渲染到map上
Food.prototype.render = function (map) {
  // 隨機(jī)食物的位置勉抓,map.寬度/food.寬度贾漏,總共有多少分food的寬度,隨機(jī)一下藕筋。然后再乘以food的寬度
  this.x = parseInt(Math.random() * map.offsetWidth / this.width) * this.width;
  this.y = parseInt(Math.random() * map.offsetHeight / this.height) * this.height;

  // 動(dòng)態(tài)創(chuàng)建食物對應(yīng)的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);
}
  • 通過自調(diào)用函數(shù)纵散,進(jìn)行封裝,通過window暴露Food對象
window.Food = Food;

創(chuàng)建蛇對象

  • Snake

  • 屬性

    • width 蛇節(jié)的寬度 默認(rèn)20
    • height 蛇節(jié)的高度 默認(rèn)20
    • body 數(shù)組隐圾,蛇的頭部和身體伍掀,第一個(gè)位置是蛇頭
    • direction 蛇運(yùn)動(dòng)的方向 默認(rèn)right 可以是 left top bottom
  • 方法

    • render 把蛇渲染到map上
  • Snake構(gòu)造函數(shù)

var position = 'absolute';
var elements = [];
function Snake(width, height, direction) {
  // 設(shè)置每一個(gè)蛇節(jié)的寬度
  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';
  }
}
  • 在自調(diào)用函數(shù)中暴露Snake對象
window.Snake = Snake;

創(chuàng)建游戲?qū)ο?/h4>

游戲?qū)ο螅脕砉芾碛螒蛑械乃袑ο蠛烷_始游戲

  • Game

    • 屬性

      • food

      • snake

      • map

    • 方法

      • start 開始游戲(繪制所有游戲?qū)ο螅?/li>
  • 構(gòu)造函數(shù)
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. 讓蛇移動(dòng)起來,把蛇身體的每一部分往前移動(dòng)一下
  2. 蛇頭部分根據(jù)不同的方向決定 往哪里移動(dòng)
Snake.prototype.move = function (food, map) {
  // 讓蛇身體的每一部分往前移動(dòng)一下
  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;
  }
  // 根據(jù)移動(dòng)的方向盐碱,決定蛇頭如何處理
  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);

讓蛇自己動(dòng)起來

  • 私有方法

    什么是私有方法把兔?
      不能被外部訪問的方法
    如何創(chuàng)建私有方法沪伙?
      使用自調(diào)用函數(shù)包裹
    
  • 在game.js中 添加runSnake的私有方法,開啟定時(shí)器調(diào)用蛇的move和render方法县好,讓蛇動(dòng)起來

  • 判斷蛇是否撞墻

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中調(diào)用
function remove() {
  // 刪除渲染的蛇
  var i = elements.length - 1;
  for(; i >= 0; i--) {
    // 刪除頁面上渲染的蛇
    elements[i].parentNode.removeChild(elements[i]);
    // 刪除elements數(shù)組中的元素
    elements.splice(i, 1);
  }
}
  • 在game中通過鍵盤控制蛇的移動(dòng)方向
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方法中調(diào)用
bindKey();

判斷蛇是否吃到食物

// 在Snake的move方法中

// 在移動(dòng)的過程中判斷蛇是否吃到食物
// 如果蛇頭和食物的位置重合代表吃到食物
// 食物的坐標(biāo)是像素聘惦,蛇的坐標(biāo)是幾個(gè)寬度某饰,進(jìn)行轉(zhuǎn)換
var headX = this.body[0].x * this.width;
var headY = this.body[0].y * this.height;
if (headX === food.x && headY === food.y) {
  // 吃到食物儒恋,往蛇節(jié)的最后加一節(jié)
  var last = this.body[this.body.length - 1];
  this.body.push({
    x: last.x,
    y: last.y,
    color: last.color
  })
  // 把現(xiàn)在的食物對象刪除善绎,并重新隨機(jī)渲染一個(gè)食物對象
  food.render(map);
}

其它處理

把html中的js代碼放到index.js中

避免html中出現(xiàn)js代碼

自調(diào)用函數(shù)的參數(shù)

(function (window, undefined) {
  var document = window.document;

}(window, undefined))
  • 傳入window對象

將來代碼壓縮的時(shí)候,可以吧 function (window) 壓縮成 function (w)

  • 傳入undefined

在將來會看到別人寫的代碼中會把undefined作為函數(shù)的參數(shù)(當(dāng)前案例沒有使用)
因?yàn)樵谟械睦习姹镜臑g覽器中 undefined可以被重新賦值诫尽,防止undefined 被重新賦值

整理代碼

現(xiàn)在的代碼結(jié)構(gòu)清晰禀酱,誰出問題就找到對應(yīng)的js文件即可。
通過自調(diào)用函數(shù)牧嫉,已經(jīng)防止了變量命名污染的問題

但是剂跟,由于js文件數(shù)較多,需要在頁面上引用酣藻,會產(chǎn)生文件依賴的問題(先引入那個(gè)js曹洽,再引入哪個(gè)js)
將來通過工具把js文件合并并壓縮。現(xiàn)在手工合并js文件演示

  • 問題1
// 如果存在多個(gè)自調(diào)用函數(shù)要用分號分割辽剧,否則語法錯(cuò)誤
// 下面代碼會報(bào)錯(cuò)
(function () {
}())

(function () {
}())
// 所以代碼規(guī)范中會建議在自調(diào)用函數(shù)之前加上分號
// 下面代碼沒有問題
;(function () {
}())

;(function () {
}())
  • 問題2
// 當(dāng)自調(diào)用函數(shù) 前面有函數(shù)聲明時(shí)送淆,會把自調(diào)用函數(shù)作為參數(shù)
// 所以建議自調(diào)用函數(shù)前,加上;
var a = function () {
  alert('11');
}
    
(function () {
  alert('22');
}())

繼承

什么是繼承

  • 現(xiàn)實(shí)生活中的繼承
  • 程序中的繼承

構(gòu)造函數(shù)的屬性繼承:借用構(gòu)造函數(shù)

function Person (name, age) {
  this.type = 'human'
  this.name = name
  this.age = age
}

function Student (name, age) {
  // 借用構(gòu)造函數(shù)繼承屬性成員
  Person.call(this, name, age)
}

var s1 = Student('張三', 18)
console.log(s1.type, s1.name, s1.age) // => human 張三 18

構(gòu)造函數(shù)的原型方法繼承:拷貝繼承(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)
}

// 利用原型的特性實(shí)現(xiàn)繼承
Student.prototype = new Person()

var s1 = Student('張三', 18)

console.log(s1.type) // => human

s1.sayName() // => hello 張三

函數(shù)進(jìn)階

函數(shù)的定義方式

  • 函數(shù)聲明
  • 函數(shù)表達(dá)式
  • new Function

函數(shù)聲明

function foo () {

}

函數(shù)表達(dá)式

var foo = function () {

}

函數(shù)聲明與函數(shù)表達(dá)式的區(qū)別

  • 函數(shù)聲明必須有名字
  • 函數(shù)聲明會函數(shù)提升怕轿,在預(yù)解析階段就已創(chuàng)建偷崩,聲明前后都可以調(diào)用
  • 函數(shù)表達(dá)式類似于變量賦值
  • 函數(shù)表達(dá)式可以沒有名字,例如匿名函數(shù)
  • 函數(shù)表達(dá)式?jīng)]有變量提升撞羽,在執(zhí)行階段創(chuàng)建阐斜,必須在表達(dá)式執(zhí)行之后才可以調(diào)用

下面是一個(gè)根據(jù)條件定義函數(shù)的例子:

if (true) {
  function f () {
    console.log(1)
  }
} else {
  function f () {
    console.log(2)
  }
}

以上代碼執(zhí)行結(jié)果在不同瀏覽器中結(jié)果不一致。

不過我們可以使用函數(shù)表達(dá)式解決上面的問題:

var f

if (true) {
  f = function () {
    console.log(1)
  }
} else {
  f = function () {
    console.log(2)
  }
}

函數(shù)的調(diào)用方式

  • 普通函數(shù)
  • 構(gòu)造函數(shù)
  • 對象方法

函數(shù)內(nèi) this 指向的不同場景

函數(shù)的調(diào)用方式?jīng)Q定了 this 指向的不同:

調(diào)用方式 非嚴(yán)格模式 備注
普通函數(shù)調(diào)用 window 嚴(yán)格模式下是 undefined
構(gòu)造函數(shù)調(diào)用 實(shí)例對象 原型方法中 this 也是實(shí)例對象
對象方法調(diào)用 該方法所屬對象 緊挨著的對象
事件綁定方法 綁定事件對象
定時(shí)器函數(shù) window

這就是對函數(shù)內(nèi)部 this 指向的基本整理诀紊,寫代碼寫多了自然而然就熟悉了谒出。

函數(shù)也是對象

  • 所有函數(shù)都是 Function 的實(shí)例

call、apply邻奠、bind

那了解了函數(shù) this 指向的不同場景之后笤喳,我們知道有些情況下我們?yōu)榱耸褂媚撤N特定環(huán)境的 this 引用,
這時(shí)候時(shí)候我們就需要采用一些特殊手段來處理了惕澎,例如我們經(jīng)常在定時(shí)器外部備份 this 引用莉测,然后在定時(shí)器函數(shù)內(nèi)部使用外部 this 的引用。
然而實(shí)際上對于這種做法我們的 JavaScript 為我們專門提供了一些函數(shù)方法用來幫我們更優(yōu)雅的處理函數(shù)內(nèi)部 this 指向問題唧喉。
這就是接下來我們要學(xué)習(xí)的 call捣卤、apply忍抽、bind 三個(gè)函數(shù)方法。

call

call() 方法調(diào)用一個(gè)函數(shù), 其具有一個(gè)指定的 this 值和分別地提供的參數(shù)(參數(shù)的列表)董朝。

<p class="danger">
注意:該方法的作用和 apply() 方法類似鸠项,只有一個(gè)區(qū)別,就是 call() 方法接受的是若干個(gè)參數(shù)的列表子姜,而 apply() 方法接受的是一個(gè)包含多個(gè)參數(shù)的數(shù)組祟绊。
</p>

語法:

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

參數(shù):

  • thisArg

    • 在 fun 函數(shù)運(yùn)行時(shí)指定的 this 值
    • 如果指定了 null 或者 undefined 則內(nèi)部 this 指向 window
  • arg1, arg2, ...

    • 指定的參數(shù)列表

apply

apply() 方法調(diào)用一個(gè)函數(shù), 其具有一個(gè)指定的 this 值,以及作為一個(gè)數(shù)組(或類似數(shù)組的對象)提供的參數(shù)哥捕。

<p class="danger">
注意:該方法的作用和 call() 方法類似牧抽,只有一個(gè)區(qū)別,就是 call() 方法接受的是若干個(gè)參數(shù)的列表遥赚,而 apply() 方法接受的是一個(gè)包含多個(gè)參數(shù)的數(shù)組扬舒。
</p>

語法:

fun.apply(thisArg, [argsArray])

參數(shù):

  • thisArg
  • argsArray

apply()call() 非常相似,不同之處在于提供參數(shù)的方式凫佛。
apply() 使用參數(shù)數(shù)組而不是一組參數(shù)列表讲坎。例如:

fun.apply(this, ['eat', 'bananas'])

bind

bind() 函數(shù)會創(chuàng)建一個(gè)新函數(shù)(稱為綁定函數(shù)),新函數(shù)與被調(diào)函數(shù)(綁定函數(shù)的目標(biāo)函數(shù))具有相同的函數(shù)體(在 ECMAScript 5 規(guī)范中內(nèi)置的call屬性)愧薛。
當(dāng)目標(biāo)函數(shù)被調(diào)用時(shí) this 值綁定到 bind() 的第一個(gè)參數(shù)晨炕,該參數(shù)不能被重寫。綁定函數(shù)被調(diào)用時(shí)毫炉,bind() 也接受預(yù)設(shè)的參數(shù)提供給原函數(shù)瓮栗。
一個(gè)綁定函數(shù)也能使用new操作符創(chuàng)建對象:這種行為就像把原函數(shù)當(dāng)成構(gòu)造器。提供的 this 值被忽略碘箍,同時(shí)調(diào)用時(shí)的參數(shù)被提供給模擬函數(shù)遵馆。

語法:

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

參數(shù):

  • thisArg

    • 當(dāng)綁定函數(shù)被調(diào)用時(shí),該參數(shù)會作為原函數(shù)運(yùn)行時(shí)的 this 指向丰榴。當(dāng)使用new 操作符調(diào)用綁定函數(shù)時(shí)货邓,該參數(shù)無效。
  • arg1, arg2, ...

    • 當(dāng)綁定函數(shù)被調(diào)用時(shí)四濒,這些參數(shù)將置于實(shí)參之前傳遞給被綁定的方法换况。

返回值:

返回由指定的this值和初始化參數(shù)改造的原函數(shù)拷貝。

示例1:

this.x = 9; 
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 返回 81

var retrieveX = module.getX;
retrieveX(); // 返回 9, 在這種情況下盗蟆,"this"指向全局作用域

// 創(chuàng)建一個(gè)新函數(shù)戈二,將"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();  // 一秒鐘后, 調(diào)用'declare'方法

小結(jié)

  • call 和 apply 特性一樣

    • 都是用來調(diào)用函數(shù),而且是立即調(diào)用
    • 但是可以在調(diào)用函數(shù)的同時(shí)喳资,通過第一個(gè)參數(shù)指定函數(shù)內(nèi)部 this 的指向
    • call 調(diào)用的時(shí)候觉吭,參數(shù)必須以參數(shù)列表的形式進(jìn)行傳遞,也就是以逗號分隔的方式依次傳遞即可
    • apply 調(diào)用的時(shí)候仆邓,參數(shù)必須是一個(gè)數(shù)組鲜滩,然后在執(zhí)行的時(shí)候伴鳖,會將數(shù)組內(nèi)部的元素一個(gè)一個(gè)拿出來,與形參一一對應(yīng)進(jìn)行傳遞
    • 如果第一個(gè)參數(shù)指定了 null 或者 undefined 則內(nèi)部 this 指向 window
  • bind

    • 可以用來指定內(nèi)部 this 的指向徙硅,然后生成一個(gè)改變了 this 指向的新的函數(shù)
    • 它和 call榜聂、apply 最大的區(qū)別是:bind 不會調(diào)用
    • bind 支持傳遞參數(shù),它的傳參方式比較特殊嗓蘑,一共有兩個(gè)位置可以傳遞
        1. 在 bind 的同時(shí)须肆,以參數(shù)列表的形式進(jìn)行傳遞
        1. 在調(diào)用的時(shí)候,以參數(shù)列表的形式進(jìn)行傳遞
      • 那到底以誰 bind 的時(shí)候傳遞的參數(shù)為準(zhǔn)呢還是以調(diào)用的時(shí)候傳遞的參數(shù)為準(zhǔn)
      • 兩者合并:bind 的時(shí)候傳遞的參數(shù)和調(diào)用的時(shí)候傳遞的參數(shù)會合并到一起桩皿,傳遞到函數(shù)內(nèi)部

函數(shù)的其它成員

  • arguments
    • 實(shí)參集合
  • caller
    • 函數(shù)的調(diào)用者
  • length
    • 形參的個(gè)數(shù)
  • name
    • 函數(shù)的名稱
function fn(x, y, z) {
  console.log(fn.length) // => 形參的個(gè)數(shù)
  console.log(arguments) // 偽數(shù)組實(shí)參參數(shù)集合
  console.log(arguments.callee === fn) // 函數(shù)本身
  console.log(fn.caller) // 函數(shù)的調(diào)用者
  console.log(fn.name) // => 函數(shù)的名字
}

function f() {
  fn(10, 20, 30)
}

f()

高階函數(shù)

  • 函數(shù)可以作為參數(shù)
  • 函數(shù)可以作為返回值

作為參數(shù)

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

函數(shù)閉包

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

作用域豌汇、作用域鏈、預(yù)解析

  • 全局作用域
  • 函數(shù)作用域
  • 沒有塊級作用域
{
  var foo = 'bar'
}

console.log(foo)

if (true) {
  var a = 123
}
console.log(a)

作用域鏈?zhǔn)纠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()
}
  • 內(nèi)層作用域可以訪問外層作用域业簿,反之不行

什么是閉包

閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)瘤礁,
由于在 Javascript 語言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量梅尤,
因此可以把閉包簡單理解成 “定義在一個(gè)函數(shù)內(nèi)部的函數(shù)”。
所以岩调,在本質(zhì)上巷燥,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁。

閉包的用途:

  • 可以在函數(shù)外部讀取函數(shù)內(nèi)部成員
  • 讓函數(shù)內(nèi)成員始終存活在內(nèi)存中

一些關(guān)于閉包的例子

示例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()())

小結(jié)

函數(shù)遞歸

遞歸執(zhí)行模型

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()

舉個(gè)栗子:計(jì)算階乘的遞歸函數(shù)

function factorial (num) {
  if (num <= 1) {
    return 1
  } else {
    return num * factorial(num - 1)
  }
}

遞歸應(yīng)用場景

  • 深拷貝
  • 菜單樹
  • 遍歷 DOM 樹

正則表達(dá)式

  • 了解正則表達(dá)式基本語法
  • 能夠使用JavaScript的正則對象

正則表達(dá)式簡介

什么是正則表達(dá)式

正則表達(dá)式:用于匹配規(guī)律規(guī)則的表達(dá)式号枕,正則表達(dá)式最初是科學(xué)家對人類神經(jīng)系統(tǒng)的工作原理的早期研究缰揪,現(xiàn)在在編程語言中有廣泛的應(yīng)用。正則表通常被用來檢索葱淳、替換那些符合某個(gè)模式(規(guī)則)的文本钝腺。
正則表達(dá)式是對字符串操作的一種邏輯公式,就是用事先定義好的一些特定字符赞厕、及這些特定字符的組合艳狐,組成一個(gè)“規(guī)則字符串”,這個(gè)“規(guī)則字符串”用來表達(dá)對字符串的一種過濾邏輯皿桑。

正則表達(dá)式的作用

  1. 給定的字符串是否符合正則表達(dá)式的過濾邏輯(匹配)
  2. 可以通過正則表達(dá)式毫目,從字符串中獲取我們想要的特定部分(提取)
  3. 強(qiáng)大的字符串替換能力(替換)

正則表達(dá)式的特點(diǎn)

  1. 靈活性、邏輯性和功能性非常的強(qiáng)
  2. 可以迅速地用極簡單的方式達(dá)到字符串的復(fù)雜控制
  3. 對于剛接觸的人來說诲侮,比較晦澀難懂

正則表達(dá)式的測試

  • 在線測試正則
  • 工具中使用正則表達(dá)式
    • sublime/vscode/word
    • 演示替換所有的數(shù)字

正則表達(dá)式的組成

  • 普通字符
  • 特殊字符(元字符):正則表達(dá)式中有特殊意義的字符

示例演示:

  • \d 匹配數(shù)字
  • ab\d 匹配 ab1镀虐、ab2

元字符串

通過測試工具演示下面元字符的使用

常用元字符串

元字符 說明
\d 匹配數(shù)字
\D 匹配任意非數(shù)字的字符
\w 匹配字母或數(shù)字或下劃線
\W 匹配任意不是字母,數(shù)字沟绪,下劃線
\s 匹配任意的空白符
\S 匹配任意不是空白符的字符
. 匹配除換行符以外的任意單個(gè)字符
^ 表示匹配行首的文本(以誰開始)
$ 表示匹配行尾的文本(以誰結(jié)束)

限定符

限定符 說明
* 重復(fù)零次或更多次
+ 重復(fù)一次或更多次
? 重復(fù)零次或一次
{n} 重復(fù)n次
{n,} 重復(fù)n次或更多次
{n,m} 重復(fù)n到m次

其它

[] 字符串用中括號括起來刮便,表示匹配其中的任一字符,相當(dāng)于或的意思
[^]  匹配除中括號以內(nèi)的內(nèi)容
\ 轉(zhuǎn)義符
| 或者绽慈,選擇兩者中的一個(gè)恨旱。注意|將左右兩邊分為兩部分抄肖,而不管左右兩邊有多長多亂
() 從兩個(gè)直接量中選擇一個(gè),分組
   eg:gr(a|e)y匹配gray和grey
[\u4e00-\u9fa5]  匹配漢字

案例

驗(yàn)證手機(jī)號:

^\d{11}$

驗(yàn)證郵編:

^\d{6}$

驗(yàn)證日期 2012-5-01

^\d{4}-\d{1,2}-\d{1,2}$

驗(yàn)證郵箱 xxx@itcast.cn

^\w+@\w+\.\w+$

驗(yàn)證IP地址 192.168.1.10

^\d{1,3}\(.\d{1,3}){3}$

JavaScript 中使用正則表達(dá)式

創(chuàng)建正則對象

方式1:

var reg = new Regex('\d', 'i');
var reg = new Regex('\d', 'gi');

方式2:

var reg = /\d/i;
var reg = /\d/gi;

參數(shù)

標(biāo)志 說明
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 = "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';
// 正則表達(dá)式中的()作為分組來使用管毙,獲取分組匹配到的結(jié)果用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);

案例:表單驗(yàn)證

QQ號:<input type="text" id="txtQQ"><span></span><br>
郵箱:<input type="text" id="txtEMail"><span></span><br>
手機(jī):<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 () {
  //獲取當(dāng)前文本框?qū)?yīng)的span
  var span = this.nextElementSibling;
  var reg = /^\d{5,12}$/;
  //判斷驗(yàn)證是否成功
  if(!reg.test(this.value) ){
    //驗(yàn)證不成功
    span.innerText = "請輸入正確的QQ號";
    span.style.color = "red";
  }else{
    //驗(yàn)證成功
    span.innerText = "";
    span.style.color = "";
  }
};

//txtEMail
txtEMail.onblur = function () {
  //獲取當(dāng)前文本框?qū)?yīng)的span
  var span = this.nextElementSibling;
  var reg = /^\w+@\w+\.\w+(\.\w+)?$/;
  //判斷驗(yàn)證是否成功
  if(!reg.test(this.value) ){
    //驗(yàn)證不成功
    span.innerText = "請輸入正確的EMail地址";
    span.style.color = "red";
  }else{
    //驗(yàn)證成功
    span.innerText = "";
    span.style.color = "";
  }
};

表單驗(yàn)證部分铆隘,封裝成函數(shù):

var regBirthday = /^\d{4}-\d{1,2}-\d{1,2}$/;
addCheck(txtBirthday, regBirthday, "請輸入正確的出生日期");
//給文本框添加驗(yàn)證
function addCheck(element, reg, tip) {
  element.onblur = function () {
    //獲取當(dāng)前文本框?qū)?yīng)的span
    var span = this.nextElementSibling;
    //判斷驗(yàn)證是否成功
    if(!reg.test(this.value) ){
      //驗(yàn)證不成功
      span.innerText = tip;
      span.style.color = "red";
    }else{
      //驗(yàn)證成功
      span.innerText = "";
      span.style.color = "";
    }
  };
}

通過給元素增加自定義驗(yàn)證屬性對表單進(jìn)行驗(yàn)證:

<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>
  手機(jī):<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>
// 所有的驗(yàn)證規(guī)則
var rules = [
  {
    name: 'qq',
    reg: /^\d{5,12}$/,
    tip: "請輸入正確的QQ"
  },
  {
    name: 'email',
    reg: /^\w+@\w+\.\w+(\.\w+)?$/,
    tip: "請輸入正確的郵箱地址"
  },
  {
    name: 'phone',
    reg: /^\d{11}$/,
    tip: "請輸入正確的手機(jī)號碼"
  },
  {
    name: 'date',
    reg: /^\d{4}-\d{1,2}-\d{1,2}$/,
    tip: "請輸入正確的出生日期"
  },
  {
    name: 'cn',
    reg: /^[\u4e00-\u9fa5]{2,4}$/,
    tip: "請輸入正確的姓名"
  }];

addCheck('frm');


//給文本框添加驗(yàn)證
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屬性的元素添加驗(yàn)證
    if (element.name) {
      element.onblur = function () {
        // 使用dataset獲取data-自定義屬性的值
        var ruleName = this.dataset.rule;
        var rule =getRuleByRuleName(rules, ruleName);

        var span = this.nextElementSibling;
        //判斷驗(yàn)證是否成功
        if(!rule.reg.test(this.value) ){
          //驗(yàn)證不成功
          span.innerText = rule.tip;
          span.style.color = "red";
        }else{
          //驗(yàn)證成功
          span.innerText = "";
          span.style.color = "";
        }
      }
    }
  }
}

// 根據(jù)規(guī)則的名稱獲取規(guī)則對象
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;
}

附錄

A 代碼規(guī)范

代碼風(fēng)格

校驗(yàn)工具

B Chrome 開發(fā)者工具

C 文檔相關(guān)工具

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卓舵,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子膀钠,更是在濱河造成了極大的恐慌掏湾,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肿嘲,死亡現(xiàn)場離奇詭異融击,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)雳窟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門尊浪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人封救,你說我怎么就攤上這事拇涤。” “怎么了誉结?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵鹅士,是天一觀的道長。 經(jīng)常有香客問我搓彻,道長如绸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任旭贬,我火速辦了婚禮怔接,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘稀轨。我一直安慰自己扼脐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瓦侮,像睡著了一般艰赞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肚吏,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天方妖,我揣著相機(jī)與錄音,去河邊找鬼罚攀。 笑死党觅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的斋泄。 我是一名探鬼主播杯瞻,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼炫掐!你這毒婦竟也來了魁莉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤募胃,失蹤者是張志新(化名)和其女友劉穎旗唁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摔认,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡逆皮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了参袱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡秽梅,死狀恐怖抹蚀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情企垦,我是刑警寧澤环壤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站钞诡,受9級特大地震影響郑现,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜荧降,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一接箫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧朵诫,春花似錦辛友、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽邓梅。三九已至,卻和暖如春邑滨,著一層夾襖步出監(jiān)牢的瞬間日缨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工掖看, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匣距,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓乙各,卻偏偏與公主長得像墨礁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子耳峦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容