对函数拓展兴趣更大一点,优先看,前面字符串后面再说,那些API居多,会使用能记住部分就好。html
1、函数参数可使用默认值数组
1.默认值生效条件浏览器
在变量的解构赋值就提到了,函数参数可使用默认值了。正常咱们给默认值是这样的:app
//ES5 function log(x, y) { y = y || "echo"; console.log(x, y); }; log('hello')//hello echo
若是y未赋值则为假,那就取后面的默认赋值,很巧妙,可是有个问题,假设我y就是想传递一个false或者一个null,结果会被当假处理,仍是执行默认赋值。函数
function log(x, y) { y = y || "echo"; console.log(x, y); }; log('hello','')//hello echo log('hello',false)//hello echo log('hello',null)//hello echo log('hello',0)//hello echo
很明显这就不是咱们想要的了,我就是想用数字0,就是想用null,结果就是赋值不上去了。怎么解决呢?这里就能够用参数默认赋值了。像这样:学习
//ES6 function log(x, y = "echo") { console.log(x, y); }; log("hello", 0);//hello 0 log("hello", null);//hello null log("hello", false);//hello false log("hello", '');//hello
原理就是,只要调用提供的参数不严格等于undefined,那就用调用传递的参数,不然才考虑使用默认值。测试
这里数字0,null,false都不严格等于undefined,因此起到了做用。优化
2.参数默认值与结构赋值的结合使用this
函数不只能够直接给参数默认值,还能结合解构赋值的玩法,来看下面的例子:es5
function foo({ x, y = 5 }) { console.log(x, y); } foo({});//undefined 5 foo();//报错
为何foo()报错了?这是由于上述代码中,{x,y=5}这一段是结构赋值的默认值,并非函数形参的默认值,函数foo都没声明xy,上哪给你输出xy去。
foo({})之因此输出正常,这是由于这种调用等同于如下代码:
function foo({ x, y = 5 } = {}) { console.log(x, y); } foo(); //undefined 5 foo({}); //undefined 5
这样写,直接调用就随便你传不传参数了,因此上面之因此输出undefined与5是由于x在解构赋值时没找到对应值,可是y因为解构赋值中传递的值严格等于undefined,因此默认值生效,这里输出了5。不理解建议重看解构赋值,应该不难理解....
这里咱们一共说了两个默认值了,解构赋值的默认值,函数形参的默认值,混着说容易糊涂,来看一个有趣的例子:
//默认值给解构赋值 function foo({ x = 1, y = 5 } = {}) { console.log(x, y); } foo(); //1 5 foo({}); //1 5 //默认值给函数形参 function foo1({ x, y } = { x: 1, y: 5 }) { console.log(x, y); } foo1(); //1,5 foo1({}); //undefined undefined
咱们分别把默认值给了解构赋值与函数形参,结果二者在相同调用状况下,仍是存在差别。
解构赋值2次都是输出1,5,理由很简单,两次传递的参数都相同于undefined,解构赋值默认值始终生效。
而默认值给函数形参,当foo1()调用时,什么都没传,解构赋值将1与5赋予给xy;
而foo1({})调用其实存在2次赋值,第一次是函数形参赋值,传递了一个空对象,直接将解构赋值右边替换了。
//step1 function foo1({ x, y } = {}) { console.log(x, y); }; foo1();
第二次就是解构赋值,犹豫xy又没赋值,又没有默认值,因此都输出undefined了。
3.参数默认值建议放在参数尾部
这个建议是考虑到参数简写的问题,若是默认值放在参数末尾,调用传参时能够省略,不然省略了会报错,举个例子:
function demo(x = 1, y) { console.log(x, y); } demo(,1)//报错
可是放在尾部就随你了,爱传不传,不传当undefined处理,正好默认值生效。
function demo(y, x = 1) { console.log(x, y); } demo(1); //1,1
4.默认值会影响函数的length属性
咱们都知道,函数的length属性会访问形参的个数。
console.log(function(a, b, c) {}.length); //3
可是若是形参使用了默认值,length就会受到影响。
console.log(function(a, b, c = 1) {}.length); //2
你觉得是有了默认值的不计算在length中了,那你就中招了,当默认值形参是第一个时:
console.log(function(a = 1, b, c) {}.length); //0
让咱们从新理解length,当形参存在默认值时,length属性会统计函数预期传入的参数个数(没默认值的参数),毕竟参数若是默认值都有了,还预期个球;其次,它不统计默认值以后的形参个数。因此上面默认值给了第一个形参,直接length为0了,这对于若是程序用了默认值,又要访问length的格外须要注意。
5.默认值会建立额外的做用域
若是函数形参使用了默认值,函数在声明初始化时,参数区域会造成一个看不见的,额外的做用域。不设置默认值不会出现这个做用域。我读到这句话觉得只有函数声明加默认值才有做用域的问题,其实函数表达式也有这种状况。
var x = 1; function f(x, y = x) { console.log(y); }; f(); //undefined f(2); //2 var x = 1; var f2 = function(x, y = x) { console.log(y); }; f2();//undefined f2(1);//1
这里最让人疑惑的就是,f()为啥不输出全局1,竟然是undefined。
缘由是y=x使用了默认赋值,建立了一个独立的做用域,y的值从x找,而本做用于中是能够找到第一个参数x的,只是它没有被赋值,等同于声明了但没给值,因此是undefined。理解不了?差很少是这个意思:
var x = 1; { let x; let y = x; console.log(x);//undefined }
但当咱们把形参x去掉时,再次调用就发生改变了:
var x = 1; function f3(y = x) { console.log(x, y); } f3(); //1,1 f3(2); //1,2
怎么这下xy都用全局的呢?由于这个独立做用域没找到x,恰好外部全局又有个,继承来了呗,等同于这个意思:
var x = 1; { //x=1 继承来的 let y = x; console.log(x, y); //1 1 }
咱们再来个极端的,看这个代码,会报错:
var x = 1; function f4(x = x) { console.log(x); } f4(); //报错
这就不用解释了,暂时性死域,未声明就开始使用,会计做用域确定不一样意啊,等同于这样:
var x = 1; { //此时里外2个x是互不相干的独立存在 let x = x; console.log(x, y); //报错 }
最后再看个稍微复杂点的例子:
var x = 1; function foo(x, y = function() {x = 2;}) { var x = 3; y(); console.log(x); }; foo(); // 3 foo(4); // 3 console.log(x); // 1
在上述代码中foo函数参数由于用了默认值,因此参数这里出现了一个独立的做用域,形参x与函数y中变量x同属于一个做用域。
而在foo函数执行体中,由于使用了var再次声明了一个x,因此这里的x与参数做用域中的x不一样,那么当咱们调用foo函数,执行了y()时,只影响了参数做用域中的x,并没影响全局x与执行体做用域的x,这里输出了3。
而当咱们把这个var去掉,执行体中的x就指向了形参x,因此输出x这里会变成2。我以为带var的状况下,有点像我在JS模式中看到的静态变量,加上形参造成独立做用域,致使二者互不干扰。
2、取代arguments的rest参数
在ES5中去获取函数形参经常会使用arguments,举个例子:
function f(){ console.log(arguments); }; f('a','b','c');//一个包含a,b,c的类数组
但在ES6中呢,新增了rest写法,好比...变量名,仍是上面的例子,就成了这样:
function f(...rest){ console.log(rest.length); console.log(Array.isArray(rest))//true }; f('a','b','c');//3
首先...后面这个变量名随便取,不是必定要写rest,其次,这个rest类型是数组!ES5的arguments是类数组,也就是说rest能够直接使用数组方法。
function f1(...rest){ return rest.sort();//虽然这个排序不严谨 }; let aa = f1(2,3,5,1);//1,2,3,5 function f2(){ // arguments.sort() 会报错 return Array.prototype.sort.call(arguments); }; let bb = f2(2,3,5,1);//1,2,3,5
咱们对任意数量数字排序,很明显...rest的写法更为精简,请忽略sort排序不严谨的地方,这里只是作个写法对比。
忘了说,rest参数本质上是获取函数额外的参数,啥意思?就是说,调用函数时,那些没能跟形参对应上的参数。
function f1(a,b,...c){ console.log(c);//[3,4,5] }; f1(1,2,3,4,5);
这个例子中,参数1,2分别与形参a,b对应,那么...c就对应额外的参数3,4,5了,应该很好理解吧。
另外,...c不算一个形参,因此咱们获取函数length属性时,是不包括rest参数的,举个例子:
function f1(a,b,...c){ console.log(f1.length);//2 }; f1(1,2,3,4,5);
最后呢,rest参数必须卸载函数形参尾部,不然就会报错。
function f1(a, ...c, b) {}; f1(1, 2, 3, 4, 5);
3、严格模式与函数name属性的部分改动
这两个简单点说,由于平时也没怎么用,作个了解就差很少了。
在ES5中,函数内部能够添加严格模式,可是ES6开始,若是这个函数使用了参数默认值,或者解构赋值,或者rest参数等,在内部使用严格模式就会报错。
这个我以为没啥说的,如今开发基本都是全局严格模式,就没函数里面玩过,谁会闲得蛋疼去函数内部定义严格模式....
关于函数的name属性,我在JS模式也简单提过,name属性其实在浏览器环境早就支持了,可是这个属性在ES6才正式归入规范...
var func = function () {}; //ES6 console.log(func.name);//func //es5 console.log(func.name);//"" //ie console.log(func.name);//undefined
咱们把一个匿名函数赋予一个变量,在ES5状况,name为空,ES6会将这个变量做为函数的name属性。其实我以为将匿名函数赋予变量不就是函数表达式的写法么。
其次,虽然ES6将函数name属性归入了规范,但部分浏览器实现仍然不一样,好比另类的IE在上面的代码中,输出竟然是undefined。
那若是咱们将一个实名函数赋予给一个变量呢,这里须要注意一下:
var func = function demo() {console.log(1)}; console.log(func.name);//1 func();//demo
此时这个函数调用要经过func来调用,但name属性倒是demo。ie获取name属性依旧是undefined
若是是构造函数实例呢,name属性就是anonymous(匿名的):
//ES6 console.log((new Function).name)//anonymous //IE console.log((new Function).name)//undefined
即使你将这个构造函数赋予给一个变量也如此:
var a = new Function(); console.log(a.name)//anonymous
4、箭头函数
1.箭头函数基本用法
ES6引入了箭头函数,大大简化了函数的写法,一个最简单的例子:
//ES5写法 var a = function (x){console.log(x)}; //箭头函数写法 var a = x => console.log(x); a(1);
var sum = function(a1, b1) { return a1 + b1; }; //箭头函数写法 var sum = (a1, b1) => a1 + b1; var a = sum(1, 2); console.log(a); //3
function与return都被省略了。
固然,箭头函数也有必定规则,假设这个函数没有形参,或者形参超过了一个,形参的圆括号就不能简写:
var a = (x, y) => console.log(x + y); var b = () => console.log(1); a(1,2);//3 b()//1
若是执行块语句有多条,那花括号就不能省略,必须加上,好比:
var sum = (num1, num2) => { var a = num1 + num2; return a;} sum(1,2)//3
或者代码块包含了对象,自己就自带了花括号,那外层的花括号确定是不能省略的。ES6入门这本书说若是箭头函数若是直接返回对象,也必须在对象外面加上花括号,我以为这句话说的不严谨,好比这样我返回对象仍是不用加:
let a = {name:'echo'} let f = () => a; const obj = f(); console.log(obj);//{name:'echo'}
其次,就算加了花括号,内层的对象也并非返回,我理解的返回就是return,这里有点歧义。
let f = () => {{name:'echo'}}; const obj = f(); console.log(obj);//undefined
箭头函数可以与解构赋值结合使用,这确定是没问题的,毕竟解构赋值也只是改变了参数传递的方式,下面两种写法做用相同
let f = ({x,y}) => console.log(x,y); var f = function (obj) { console.log(obj.x,obj.y) };
ES6箭头函数写法最重要的就是大大减小了回调函数的代码量,毕竟回调使用频率过高了,好比forEach回调:
const arr = [1,2,3,4]; arr.forEach((element,index) => { console.log(index+':'+element); });
2.箭头函数带来的使用改变
箭头函数带来便捷的同时,也改变了部分规则。咱们都知道this这个东西永远指向它最终的调用者,可是这条规则在箭头函数中失效了。
var me = {name:'echo'}; var name = '时间跳跃' let f1 = function (){ console.log(this.name)} let f2 = () => console.log(this.name); f1.call(me);//echo f2.call(me);//时间跳跃
上述代码,我定义了2个相同的函数,只是一个是箭头函数的写法,f1输出echo毋庸置疑,函数执行时,this指向了me对象,因此name属性是echo。
箭头函数呢,即使咱们使用了call方法,但执行时this依旧指向了window,因此拿到了时间跳跃。
那么问题来了?为啥箭头函数的this指向了全局window?
首先咱们得明白几个概念:
第一:准确来讲,箭头函数没有本身的this,它的this是从定义了它的外层代码块那里借来的,读书人的说法不叫偷。
第二:箭头函数的this是静态的,从定义好开始,this就老实本分的只从箭头函数外的做用域借,不受其它诱惑。
那么咱们回头看上面的代码,来应用这两个概念,第一f2函数没有本身的this,它从构造函数外层做用域借,外层是谁?外层是全局,这里的全局就是window。随便此时咱们经过call修改了this指向,很不巧,我箭头函数的this就是死了心的从外层借。
为了证明这两个观点,咱们来看两个例子,首先是普通函数:
function f(){ console.log(this);//{a:1} setTimeout(function () { console.log(this);//window },100); }; f.call({a:1});
函数f被调用时,this确定指向{a:1},因此函数f中输出this,指向了该对象。而定时器中的函数输出时,this是指向window,毕竟定时器中的函数有点自调的意思,相似于这样:
function f() { console.log(this); //{a:1} (function() { console.log(this);//window })(); } f.call({ a: 1 });
定时器中的函数就差很少这个意思了,普通写法天然this天然指向window。
如今咱们将定时器中的函数修改成箭头函数,箭头函数没this,要从外层做用域借,外层的是对象{a:1},因此这里箭头函数应该也输出此对象:
function f() { console.log(this); //{a:1} setTimeout(() => { console.log(this); //{a:1} }, 100); } f.call({ a: 1 });
测试一下,果真没问题,那么this就先说到这里了。
除了this的变化,箭头函数不能用在构造函数上,毕竟箭头函数没this啊,this都是借来的,this都没有,还构造个球。
其次,箭头函数没有arguments对象,若是要用就得使用rest参数代替,这个前面也有说。
最后,箭头函数不能使用yield命令,这个我不是很了解,后面看了再说吧。
5、双冒号运算符(函数绑定运算符)
函数绑定运算符是经过两个冒号::来取代call,bind,apply方法绑定this的一种提案。
函数绑定运算符左边是对象,右边是一个函数,那么函数执行时,函数的this也就是执行上下文将指向左边的对象。
obj::func; // 等同于 func.bind(obj);
可是这个貌似还不可用,双冒号直接报错了,先做为了解吧。
6、尾调用优化(只在严格模式生效)
1.尾调用
尾调用是指在函数执行的最后一步调用另外一个函数并return。
function f(a){ return f2(a); };
就是说,最后执行的一步,必定是单纯的调用了某个函数并返回了。只要加了其它操做的都不叫尾调用:
function f(a){ f2(a); }; function f(a){ return f2(a)+1; }; function f(a){ let func2 = f2(a); return func2; };
第一个没return函数,本质上最后一步隐性返回了一个undefined,第二个除了调用函数还有加法的操做,第三个最后一步单纯return没调用,都不算尾调用。
另外,除了要求最后一步调用函数外,内部函数被调用时还不能依赖外层函数的内部变量,不然也不属于尾调用:
function f1(data) { var a = 1; function f2(b){ return b + a; }; return f2(a); }
那么这个尾调用能带来什么优化?意义是啥?
函数调用时会在内存中造成一个调用记录,又叫调用帧call frame,用于保存调用的位置与内部变量等信息。
举个例子,咱们首先调用函数A,而函数A又要调用函数B,那么在内存中,A的调用帧上面会有一个函数B的调用帧。此时A,B的调用帧组合起来就造成了一个调用栈call stack。
等到函数B执行完成会将执行结果返回到函数A,函数B的调用帧消失,再执行函数A,完成后A的调用帧消失。画的比较丑,大概这么个意思:
尾调用比较奇妙的因为它是最后一步调用,好比上面的B,它不会再记录额外的信息,也不会建立额外的调用帧,很是节约内存。
2.尾递归
什么函数调用特别消耗内存?首要想到的---递归,递归这个东西由于要本身调用本身,处理很差就有栈溢出的问题;那咱们能不能让递归结合尾调用来解决递归自身函数调用时内存消耗过大的问题,固然能够,这种玩法也叫尾递归。
尾递归每次调用本身都是最后一步的操做,所以根本不会建立更多的调用帧,完美解决栈溢出的风险,固然,尾递归须要改写本来递归的函数。
咱们实现一个简单阶乘函数:
//递归 function f(a) { if (a === 1) { return a; }; return a * f(a - 1); }; f(5);//120 //尾递归 function f(a, total) { if (a === 1) { return total }; return f(a - 1, a * total); }; f(5, 1);//120
很明显,普通递归最后一步还处理了乘法运算,不知足尾调用,而改写以后,咱们将计算的部分交给了形参,再次调用时,已是干净的函数调用返回了,这就是尾调用。
我在 从斐波那契数列浅谈递归有简单说起斐波那契数列与递归,这里咱们也能经过尾递归改写斐波那契数列的计算:
//普通递归 比我写的递归好多了.... function Fibonacci(n) { if (n <= 1) { return 1; } return Fibonacci(n - 1) + Fibonacci(n - 2); }; Fibonacci(10)//89 // 尾递归 function Fibonacci2(n, ac1 = 1, ac2 = 1) { if (n <= 1) { return ac2; } return Fibonacci2(n - 1, ac2, ac1 + ac2); }; Fibonacci(10)//89
由于我对于递归使用不是很熟练,有些时候甚至用递归实现都比较难,这个仍是得培养,这里就只传达这个思想了。
忘了说,尾递归如今谷歌还不支持,兼容性并非全部浏览器都实现了,可是知道也没坏处。
那么这章到这里结束了,我竟然写了这么长,鬼看的下去,算了...纯当本身学习记录了。
若是你对函数参数默认值产生的独立做用域这个概念有所疑虑,欢迎阅读博主这篇文章,保证能让你看懂: