es6入门3--箭头函数与形参等属性的拓展
对函数拓展兴趣更大一点,优先看,前面字符串后面再说,那些API居多,会使用能记住部分就好。
一、函数参数可以使用默认值
1.默认值生效条件
在变量的解构赋值就提到了,函数参数可以使用默认值了。正常我们给默认值是这样的:
//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.参数默认值与结构赋值的结合使用
函数不仅可以直接给参数默认值,还能结合解构赋值的玩法,来看下面的例子:
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模式中看到的静态变量,加上形参形成独立作用域,导致两者互不干扰。
二、取代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);
三、严格模式与函数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);//demo func();//1
此时这个函数调用要通过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
四、箭头函数
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
或者代码块包含了对象,由于对象本身就自带了花括号,那外层的需要使用圆括号进行包裹。
let f = () => ({name:'echo'}); const obj = f(); console.log(obj);//{name: "echo"}
箭头函数能够与解构赋值结合使用,这肯定是没问题的,毕竟解构赋值也只是改变了参数传递的方式,下面两种写法作用相同
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命令,这个我不是很了解,后面看了再说吧。
五、双冒号运算符(函数绑定运算符)
函数绑定运算符是通过两个冒号::来取代call,bind,apply方法绑定this的一种提案。
函数绑定运算符左边是对象,右边是一个函数,那么函数执行时,函数的this也就是执行上下文将指向左边的对象。
obj::func; // 等同于 func.bind(obj);
但是这个貌似还不可用,双冒号直接报错了,先作为了解吧。
六、尾调用优化(只在严格模式生效)
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
因为我对于递归使用不是很熟练,有些时候甚至用递归实现都比较难,这个还是得培养,这里就只传达这个思想了。
忘了说,尾递归现在谷歌还不支持,兼容性并不是所有浏览器都实现了,但是知道也没坏处。
那么这章到这里结束了,我居然写了这么长,鬼看的下去,算了...纯当自己学习记录了。
如果你对函数参数默认值产生的独立作用域这个概念有所疑虑,欢迎阅读博主这篇文章,保证能让你看懂: