JavaScript基础(6)—— 花里胡哨的函数

  在正式开始总结JavaScript权威指南第八章——函数之前,先来谈谈在实际应用中函数是做什么的。

  通常情况下,我们会将一些常用的工具(如数学方法,数据格式处理,接口处理工具)封装成一段可重复执行的代码,这种只定义一次就可以通过函数名被多次执行的代码就叫做函数。说白了,我们使用函数就是为了少写几行重复代码。

  FBI warning:本文要介绍的函数跟复用没有半毛钱关系,如果追求实用主义,完全可以跳过本章内容!

  下面开始正式介绍花里胡哨的函数技巧,这些技巧大致包含以下内容:

  1.嵌套函数和听起来很深奥的闭包

  2.三种修改函数上下文(this)的调用(apply,call,bind)

  3.两种看起来很人性化的链式调用和函数柯里化

  4.函数和跟他一点都不像的对象类型之间的爱恨瓜葛

  5.听起来很牛逼其实就是函数处理函数的高阶函数

  6.听起来很奇怪其实跟函数柯里化差不多的不完全函数

  7.可能有一丢丢软用的函数记忆

  文章实际内容安排跟书本顺序有较大不同,看不懂的建议看原著,你会更看不懂。

  

1.一个函数的基本信息

  根据个人整理,一个函数至少包含以下信息:函数体,参数信息(arguments),length属性(函数实参数量),上下文(this),函数值(返回值,如果没有返回值默认是undefined),call()和apply()方法以及从Function.prototype继承的原型。

  上面提到的函数基本信息里,常用的只有函数体和实参信息,其他信息我们平时基本都用不到,但如果你想要写出一个好用且健壮的工具库,这些方法就变得十分重要了。在本章内容中,我会多次用到上面这些函数的基本属性和方法,有些东西看似鸡肋,但在某些情况下会变得十分有用,因此除了要了解函数的这些基本属性,最重要的还是要灵活运用!一本正经的抄书从来不是这个系列的目的,我希望各位读者在看完本章后,会对函数有一个全新的认识(包括我自己)。下面开始一本正经的胡说八道。

2.闭包的不详细说明

  闭包是JavaScript面试题中永恒的主题,可见闭包在实践中是多么糟粕的一种存在,当然我今天要说的不是实践,这个系列也不是服务于实践,但是关于闭包,我只谈个人看法(理解有明显错误可以评论指出)。

  用一个词解释“闭包”:就是局部作用域!在块级作用域出来之前,JavaScript一直使用函数作用域。当我们使用函数时,就会产生一个“封闭”在函数中的局部作用域,因此所有的函数都可以称之为闭包,在块级作用域出来之后,你也可以认为下面的代码存在“闭包”:

if(true){
  const b = 0 // 解析到const,使用块级作用域,也就是大括号作用域,所以if语法内产生了闭包
  console.log(b)
}

  因此,“闭包”在广义上并不是一个很难理解的东西,只要产生了局部作用域,就可以称之为闭包。但广义的闭包显然不是各位想要了解的内容,各位想要了解的闭包,称之为——嵌套函数。

  然而嵌套函数只是函数嵌套函数的书本名词,大部分情况下他依旧不能满足各位对于闭包的臆想,各位想要了解的闭包,是嵌套函数的一种特殊情况——函数内局部变量的“保存”,下面举个最简单例子,来详解一下各位想要了解的闭包

function increment(){
	let count = 0
	return function(){
		return count++
	}
}
let incrementNum = increment()
incrementNum() //0
incrementNum() //1

  在上例中,函数声明内部的变量被“保存”在函数作用域内,这个变量没有被垃圾回收机制自动回收,对这个环节感兴趣的可以去了解下JavaScript函数的辣鸡回收机制,本例中只简单解释下为什么count局部变量可以驻扎在内存中:

  1.因为increment()函数返回一个匿名函数,这个函数被全局变量incrementNum引用,因此这个匿名函数没有被回收。

  2.由于匿名函数内部使用的变量指向increment()所创建的局部作用域对象中的count属性,因此count也被强制没法回收了。

  说来说去可能把某些初学者搞得云里雾里了,想要真正的了解闭包,只需要记住一句话就够了:

  函数的作用域只跟函数定义时的作用域有关!

  要理解这段话,可以看下面的例子:

var scope = 'world'
function a(){
	console.log(scope) // undefined
	var scope = 'hello' // 注意这里不要用let,严格模式下变量不能提升,会导致报错
	return function(){
		console.log(scope)
	}
}
a()() //hello

 在上例中,定义了两个函数,一个是嵌套函数a,另一个是匿名函数,嵌套在a函数中。

 a函数定义的时候,他的作用域对象是这样子的:

//a函数的作用域链对象
a的爸爸全局作用域:{
  scope:'world',
  ...
  a自己的作用域:{
    scope:'hello'
    ...
    a的儿子匿名函数的作用域:{
        空空如也
    }
  }
}

  遵循优先找自己作用域内的属性,其次找父级作用域的属性,直到全局作用域也找不到返回undefined的套路,我们可以得出以下结论,a函数中打印scope的时候,由于变量提升,因此a函数判断自己的作用域内存在scope变量,不需要寻求父级帮助,但在打印scope的时候,scope还没被赋值,因此打印undefined。匿名函数在自己的作用域内找不到scope变量,因此寻求父级a的帮助,找到后发现scope='hello',因此匿名函数打印'hello',而不是全局变量'world'。细细体会这两个例子,就可以完全了解闭包了。

3.call(),apply() 和bind()

  在百度搜索call(),apply()和bind()的区别,类似的文章有一大堆,这里我啰嗦两句,给自己做个笔记。

  call(),apply()和bind()都可以看作是函数对象的方法,我们可以通过调用函数对象的call(),apply()方法来实现函数的调用。call和apply的第一个参数会替代函数的原始上下文this,注意this是函数内部的关键字,  而不是一个普通变量。下面通过一个简单例子来了解下call和apply。

function a(){
  console.log(this.x)
}
a()//undefined this指向window
a.call({x:1}) //1
a.apply({x:1}) //1

 从上例中可以看出,call和apply都可以修改函数的上下文,两者唯一的区别就在于函数传参的形式不同,如下所示

function a(x,y){
  console.log(x,y,this.z)
}
a.call({z:3},1,2) //1,2,3
a.apply({z:3},[1,2])//1,2,3

  现在你可以使用对象展开符...[1,2]来实现两者的转换。

  bind()方法也可以修改函数上下文,但是bind方法返回一个新的函数,而不是直接调用函数,如下所示。

function a(x,y){
  console.log(x,y,this.z)
}
var newFn = a.bind({z:3})
newFn(1,2) //1,2,3

4.一句话搞定链式调用

  链式调用时函数式编程的技巧之一,其核心代码却只有一句话,就是return this。链式调用往往注重过程而不注重结果,类似于juery就是借鉴了这种技巧。(我不是说jquery的所有方法都是return this)我们可以手写一个最简单的链式调用,如下:

let myMath = {
	x:1,
	y:1,
	addX: function(){
		this.x++
		return this
	},
	addY:function(){
		this.y++
		return this
	}
}
let a = myMath.addX().addY().addX().addY().addX()
console.log(a.x,a.y) //4 3

5.函数柯里化是什么?

  函数柯里化这个名词对于没怎么经历过笔试和书本的人来说,是个非常陌生的词。

  在我的文章里,你永远不用去关注某个专业术语的名称,你只需要知道,函数还可以这样“优化”就可以了。

  那么函数柯里化究竟做了一件什么样的事情呢?举例说明:

  我们需要一个函数,它可以传递任意个参数,且函数可以被一直调用,直至我们不需要继续调用函数为止。

  比如我们需要知道一辆出租车一天的公里数,我们可以将一天分为白天和黑夜,白天统计一次里程数,晚上也统计一次里程数,函数调用如下

  sum(白天的里程数)(晚上的里程数)

  我们也可以把一天分为24小时,函数调用如下

 sum(0:00)(1:00)(2:00) ....... (23:00)(24:00)

  我们还可以把上午分为8小时,中午分为8小时,晚上分为8小时,调用如下

  sum(0:00,1:00,2:00...)(9:00,10:00,....)(17:00,18:00,...,24:00)

  可以看到,sum函数的参数个数和调用次数都是未知的,因此我们需要解决两个问题,如何解析参数以及如何多次调用函数。第一个问题很好解决,在函数的基本信息里包含arguments(参数信息),因此我们的sum函数不需要规定形参的个数,可以直接通过arguments获取参数。第二个问题是如何让sum函数一直调用本身,答案也很简单,利用递归的思想即可实现,这个问题的关键就是递归结束条件是什么,也就是,我们如何得知本次调用是最后一次调用?答案是:没办法!

  那咋办呢?自己看代码,自己百度,我也不知道toString()方法为什么能作为递归结束的条件,百度上就是这么写的。当然我们也可以通过非常朴素的方法:if(arguments.length===0)来作为结束条件,只是这样你需要多调用一次函数,如下:

  sum(1,2,3)(4,5)(5,5)(),结果和下面的代码时相同的,只是下面的调用方式看起来更加“优美”一些。

function sum(){
	let args = Array.from(arguments)
	function add(){
		args = args.concat(Array.from(arguments))
		return add
	}
	add.toString = function(){
		return args.reduce((total,current)=>{
			return total + current
		})
	}
	return add
}
let sums = sum(1,2,3)(4,5)(5,5) // 25

  其实这里还用到了一些闭包的技巧,add()函数使用了sum()的局部变量,并且在不断地递归中,这个变量可以累积得到所有参数的值,直到最后一次调用时,把所有参数进行累加。

6.函数也是对象,所以我们要利用起来

  这一小节的关键字就5个字:函数是对象!

  至于怎么使用,我会在记忆函数中提到。

 7.高阶函数

  所谓高阶函数,就是操作函数的函数,它接收一个或多个函数作为参数,返回一个新的函数。依旧以加减乘除为例:

function plus(a,b){
	return a+b
}
function reduce(a,b){
	return a-b
}
function mix(plus,reduce){
	return function(a,b,c,d){
		return plus(a,b)*reduce(c,d)
	}
}
let multiply = mix(plus,reduce)
console.log(multiply(1,2,6,4)) //6

8.利用函数的对象属性实现记忆

  在讲函数闭包的时候我们提到了“函数内变量驻扎”的概念,

  说到保存一个变量在函数内,其实有两种方法,但这两种方法是有区别的:

  第一种就是闭包

  第二种就是利用函数的对象属性

  函数作为对象,本身就可以存储属性,比如

function fn(){}
fn.x = 0 

  因此当我们需要一个一直被记忆的变量时,就可以把它存储在函数属性中,在阶乘函数里就可以用到这个特性。闭包和记忆的不同在于,闭包函数在每次调用时都会创建一个新的变量,而记忆的变量是函数的私有属性,不会被“重置”。

  函数部分就讲到这,其实还有很多没用的东西没讲,包括我讲的,大部分也没什么用,以后写框架的时候用到再体会和细说吧。

发布了109 篇原创文章 · 获赞 196 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/dkr380205984/article/details/101269720
今日推荐