《前端之路》之 JavaScript 高级技巧、高阶函数(一)

418 阅读8分钟

一、高级函数

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 、欢迎推荐 : )

前端 高级技巧、高阶函数 (一)