《前端之路》之 JavaScript 高階技巧、高階函式(一)

SmallW發表於2018-08-28

一、高階函式

1-1 安全的型別檢測

想到型別檢測,那麼腦海裡第一反應應該就是在 Javascript 的世界中到底有哪些型別(這真的是一個非常古老的問題了)

我們大致分為 2 類: 基本型別 和 引用型別

其中 基本型別 包括了: string、number、bool、undefined、null

其中 引用型別 包括了: Array、Function、Object
複製程式碼

那我們用 type 和 instanceof 分別來看下這幾種資料型別判定返回的內容 為什麼說 利用 type 和 instanceof 是不安全的型別檢測

const str = 'test'
const num = 12
const bool = false
const unde = undefined
const nulls = null
const Array = [1,2,3,4]
const Object = {name: 'zzz'}

const checkType = (type) => {
	return typeof(type)
}

// 使用 type 來判斷
console.log(checkType(str))			// string
console.log(checkType(num))			// number
console.log(checkType(bool))		// boolean
console.log(checkType(unde))		// undefined
console.log(checkType(nulls))		// object
console.log(checkType(Array))		// object
console.log(checkType(Object))		// object
// 很顯然 null、Array、Object 返回的都是 object 不夠安全 ( bug 點 )

// 用 instanceof 來判斷

const checkInstance = (type) => {
	return type instanceof String
}

console.log(checkInstance(str))	// 是 false 這是為什麼呢?

// 那麼我們就需要來介紹下 instanceof 的原理了。

複製程式碼

1-1-1 instanceof 的原理

instanceof 的的功能實現是 前者是否為後者的例項 , 具體的程式碼就是: eg:


let res = a instanceof A

// a 是 A 的例項 
// A 是 a 的建構函式
// 同時 a 的 __proto__  == A 的 prototype 那麼  a instanceof A  == true 否則就等於 false
複製程式碼

其中 有幾個關鍵的點 如下:

  • 關於 constrcutor 、proto 、prototype、原型物件 這四個點的理解。

  • 推薦一篇好文章吧 prototype、proto、constructor 的三角關係

  • 回到上面 a.proto 指向的就是 a 的原型物件

  • A.prototype 指向的是 例項物件 的 原型物件

var Foo = function() {
	this.setName = (name) => {
		this.name = name
	}
}

var foo = new Foo

Foo.prototype  指向 => 原型物件(理解為公共物件)
// 通過同一個建構函式例項化的多個物件具有相同的原型物件。經常使用原型物件來實現繼承

Foo.prototype.constructor  指向 => 建構函式本身(Foo)

foo.__proto__  指向 => 原型物件(理解為公共物件)

foo.constructor 指向 => 建構函式 (Foo)
複製程式碼

1-2 作用域安全的建構函式

在全域性作用域內呼叫函式建構函式,由於沒有使用new,導致在全域性作用域新增冗餘的屬性


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

// 假如為使用 New 操作

var person = Person('zhangsan','sell') 

console.log(window.name, window.job)	// zhangsan sell
console.log(person.name, person.job)	// VM76:11 Uncaught TypeErrorr
複製程式碼

這個問題是由this物件的晚繫結造成的 因此,需要在函式裡面確認 this物件是正確型別的例項:

function Person(name){
    if(this instanceof Person){
	    this.name = 'zhang';
    } else {
        return new Person(name)
    }
}

var person = Person('zhang')
console.log(window.name)	//  ''
console.log(person.name)	//  zhang
複製程式碼

1-3 惰性載入函式

惰性載入表示函式執行的分支會在函式第一次呼叫的時候執行,在第一次呼叫過程中,該函式會被覆蓋為另一個按照合適方式執行的函式,這樣任何對原函式的呼叫就不用再經過執行的分支去進行判斷了。(節約算力)

1-3-1 應用場景

1、 AJAX 在不同瀏覽器下相容性 2、 APP 內嵌 H5 不同環境下同一種功能方法,寫法不一樣 3、 H5 在不同平臺下多處表現形式因為一個方法而展現的不一樣。

1-3-2 注意的地方

1、應用越頻繁,越能體現這種模式的優勢所在 2、固定不變,一次判定,在固定的應用環境中不會改變 3、複雜的分支判斷,沒有差異性,不需要應用這種模式

1-3-3 Demo


const getCurEnv = () => {
	// 當前環境為 chrome 瀏覽器環境
	return window.navigator.userAgent.toLowerCase().match(/chrome/i) !== null
}

const Person = function(name) {
	this.name = name
}

const http = {
	created: function() {
		if (getCurEnv()) {
			console.log(this)
			this.created = function() {
				console.log('test1')
				return new Person('zhang1')
		    }
		    console.log('test2')
		    return new Person('zhang2')
		} else {
			this.created = function() {
				console.log('test3')
				return new Person('zhang3')
		    }
		}
	},
	Atest: ()=> {
		console.log(this)    // window {}
	},
	Ftest: function() {
		console.log(this)    // http {}
	}
}

http.created()	// test2 Person {name: "zhang2"}
http.created()  // test1 Person {name: "zhang1"}

// 實際有效的 惰性載入函式 上面的 二個 方法返回的值 其實是一樣的。這樣惰性載入函式 才是真實有效。
複製程式碼

1-4 函式繫結

這個技巧常常和回撥函式與事件處理一起使用,以便在將函式作為變數傳遞的同時保留程式碼執行環境

很多JavaScript庫實現了一個可以將函式繫結到指定環境的函式,這個函式一般都叫做bind()。一個簡單的bind()函式接受一個函式和一個環境,並返回一個給的環境中呼叫給定函式的函式,並且將所有引數原封不動傳遞過去。這個函式返回的是一個閉包。

上面的語言描述總是很虛無飄渺,我們來直接上Demo:

1-4-1 Demo


var obj1 = {
	name: 'zhang',
	getName: function() {
		console.log(arguments[0][2], 'obj1')
		return this.name
	}
}

var obj2 = {
	name: 'lisi',
	getName: function() {
		console.log(arguments, 'obj2')
		return this.name
	}
}

function Bind(fn, context) {
	return fn.call(context, arguments)
}

Bind(obj1.getName,obj2,'xxxxx')	

// Arguments [Arguments(3), callee: ƒ, Symbol(Symbol.iterator): ƒ] "obj1"
// 'lisi'
// 這裡我們對於 arguments 的 理解和操作來說都是比較陌生,那麼下面 我們再來介紹下
// arguments 具體是什麼。
複製程式碼

1-4-2 arguments

類陣列 (Array-like)

  • 可以用下標訪問每個元素
  • 有 length 屬性
  • arguments 的資料型別為 object
  • 可以使用 for 和 for-in 方法
  • 不具備 Array 原生方法

Demo


var test = function() {
	console.log(arguments)
	console.log(arguments[0])
	console.log(arguments.length)
	console.log(typeof arguments)
	for(var i = 0; i<arguments.length; i++) {
		var ele = arguments[i]
		console.log(ele)
    }
	for(x in arguments) {
		console.log(arguments[x])
	}
	// arguments.split(' ')
	// Uncaught TypeError: arguments.split is not a function
}

test(1,2,3,4)
// Arguments(4) [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// 1
// 4
// object
// 1 2 3 4
// 1 2 3 4
複製程式碼

將類陣列 轉化為 陣列

  • 方法一 :
var test = function() {
	console.log(arguments)
	var arrArg = Array.prototype.slice.call(arguments)
	console.log(arrArg)
}

test(1,2,3,4) // [1, 2, 3, 4]
複製程式碼
  • 方法二 :
var test = function() {
	console.log(arguments)
	var arrArg = Array.from(arguments)
	console.log(arrArg)
}

test(1,2,3,4) // [1, 2, 3, 4]
複製程式碼

1-4-3 ES5 中原生 bind() 方法 詳解

文字解釋起來還是比較吃力,那麼我們還是 showCode~ Demo:

var obj = {
	a: 1,
	b: 2,
	getCount: function(c, d) {
		return this.a + this.b + c + d
	}
}
console.log(obj.getCount(3,4))	// 10
window.a = window.b = 0
var funcs = obj.getCount
funcs(3,4)						// 7

複製程式碼

bind是function的一個函式擴充套件方法, bind 以後程式碼重新繫結了 func 內部的 this 指向(obj) 相容 IE9 + Demo:

var obj = {
	a: 1,
	b: 2,
	getCount: function(c, d) {
		return this.a + this.b + c + d
	}
}
console.log(obj.getCount(3,4))	// 10
window.a = window.b = 100
var funcs = obj.getCount.bind(obj)
funcs(3,4)			// 10
// var funcs = obj.getCount.bind(window)
// funcs(3,4)		// 207
複製程式碼

1-5 函式柯里化

又稱部分求值。柯里化其實本身是固定一個可以預期的引數,並返回一個特定的函式,處理批特定的需求。 這增加了函式的適用性,但同時也降低了函式的適用範圍。 文字的定義始終讓人難以接受,還是 showCode 吧 Demo:假設你要寫一個 記賬的工具,然後記錄每天的資料,最後統計整個星期的資料。 how ?

let weekCost = 0
const cost = function(num) {
	weekCost += num 
}
cost(100)	// 100
cost(200)	// 300
cost(300)	// 600
複製程式碼
這個時候每天都會進行一次 總賬,這個是我不想看到的,因為不想每天都被這個總賬看著心痛,畢竟工資不夠花是常態。我就希望每個星期給我來一次總賬刺激。
複製程式碼
const currying = function(fn) {
    let args = []
	return function() {
	    if (arguments.length == 0) {
		    return fn.apply(this, args)
	    } else {
			let test = [].push.apply(args,arguments)
			// return fn.call(this, arguments)
	    }
	}
}

const costs = (function() {
	let money = 0
	return function() {
		money = 0
		for(let i = 0; i<arguments.length; i++) {
			money += arguments[i]
		}
		return money
	}
})()

let cost = currying(costs)

cost(100)
cost(100)
cost(100)
cost(100)
cost(100)

console.log(cost())	// 500

cost(100)
cost(100)

console.log(cost())	// 700
複製程式碼

小結一:

上面的 dmeo 中,當呼叫 cost() 時,如果明確帶上引數,表明此時並不進行真正的求值計算,而是把這些引數儲存起來,此時讓 cost() 函式返回另外一個函式。只有當我們以不帶引數的形式執行 cost() 時,才利用前面儲存的所有引數,真正開始求值計算。這是一個具象的函式顆粒化的方法。那麼我們想把函式顆粒化抽象出來又需要怎麼來概括吶? ?

下面的例子,我們再來看下這個顆粒化!
複製程式碼

Demo

const currying = function(fn) {
	let args = Array.prototype.slice.call(arguments, 1)
	return function() {
		let innerArgs = Array.prototype.slice.call(arguments)
		return fn.apply(this, args.concat(innerArgs))
	}
}

const add = function(n, m) {
	return n + m
}

var curriedAdd = currying(add, 3)

console.log(curriedAdd(5)) // 8

複製程式碼

小結二:

這個例子中,通過顆粒化 建立已經設定好了一個或多個引數的函式。
後續還會有更多的例子,來證明這個點。
複製程式碼

注意

無論是柯里化函式或是繫結函式,都會帶來額外的開銷,所以不應濫用。

1-6 反函式柯里化

相反,反柯里化的作用在與擴大函式的適用性,使本來作為特定物件所擁有的功能的函式可以被任意物件所用.

核心:

通過 uncurrying 函式,講原本只能用於某個物件的方法,擴充套件到更多的 物件可以引用。 showCode:


Function.prototype.uncurrying = function() {
    var that = this;
    return function() {
        return Function.prototype.call.apply(that, arguments);
    }
}

const test1 = {}
const test2 = {}
 
test1.sayHi = function () {
    return "Hello " + this.value +" "+[].slice.call(arguments);
}

test2.sayHiuncurrying = test1.sayHi.uncurrying()

console.log(test2.sayHiuncurrying({value:'world'},"hahaha"));

// Hello world hahaha
複製程式碼

核心的程式碼已經展示出來了, 仔細的品讀品讀~

好了,今天就先寫到這裡,後面會繼續完善對於 Demo 的解釋,不明白的可以留言討論~

GitHub 地址:(歡迎 star 、歡迎推薦 : )

前端 高階技巧、高階函式 (一)

相關文章