Function 函数
函数默认参数
es5无法设置默认参数,只能通过函数内部先进行判断,如果没有参数则设置一个值。
function test(x,y){
x = x || "hello ";
y = y || "word";
console.log(x+y);
}
test(); //hello word
这样写的话会有一个问题,就是如果我就要参数为空,就会出现问题
test("","test"); // hello test
所以我们还需要加一个判断
function test(x,y){
x = typeof x === "undefined" ? "hello ": x;
y = typeof y === "undefined" ? "word": y;
console.log(x+y);
}
test("","test"); //test
但这样太麻烦了,所以es6提供了函数函数的默认值写法
function test(x="hello ",y="word"){
console.log(x+y);
}
test(); //hello word
test("","test"); //test
注意事项
- 默认参数不能在函数里面二次声明,否则报错
- 使用了默认参数,所有参数名必须唯一不能重复,否则报错
- 默认参数是惰性的,他会随着每次调用该函数,重新计算,而不是一开始就计算好
function test(x="hello "){
const x = "test"; //报错
}
function test(x="hello",x){ //报错
}
let x = 99;
function test(p=x+1){ //每次都会重新获取x的值
console.log(p);
}
test(); //100
x = 100;
test(); //101
默认参数与解构混合使用
在函数参数那,我们也可以使用解构来直接获取某些数据,并且可以和默认参数一起使用。
function test({x,y=5}){
console.log(x,y);
}
test({}); //undefined,5
test({x:1}); //1,5
test({x:1,y:2}); //1,2
test(); //报错,无法解构
这里我们是对解构的某个值进行默认参数的,事实上我们还可以对整个参数进行默认值设置
function test({x,y=5}={}){
console.log(x,y);
}
text(); //undefined,5
这就形成了双重默认值,并且我们还需要理解下他是什么样的流程。
首先我们并没有传参数,所以这里先使用了第一个默认参数{}
,一个空的对象,然后才会进行结构,运行{x,y=5}
这部分。
你不能理解为没有参数,解构这部分也直接被{}
空对象替换了,可以理解为,替换的只是解构的外层包裹,内层还是一样的,不会消失不见。
这里写两种写法:
fucntion m1({x=0,y=0} = {}) {
console.log(x,y);
}
fucntion m2({x,y} = {x:0,y:0}) {
console.log(x,y);
}
m1和m2的区别就是,第一个初始默认值不同。
m1的第一个默认值是空对象{}
,再解构的时,如果没有对应的值使用默认值。
m2的第一个默认值是带有参数的{x:0,y:0}
,然后解构时直接解构第一个默认值。
m1和m2两个函数在没有传参的情况下,结果是相同的。
但是当我们传入一个空对象时,m2由于解构不到参数,会使一个undefined
m1({}); //0,0
m2({}); //undefined,undefined
因为m1的默认值是在解构的时候设置,而m2是之前就预设,此时我们传入的了对象参数,m2的默认值就拿不到了。
那么我们传入一个不相干的参数结果也会是一样的
m1({z:1}); //0,0
m2({z:1}); //undefined,undefined
默认值参数的位置
通常情况下,定义了默认值得参数,一般是可以省略不传的,所以要写在最后面,因为在函数里面,最后那个是可以不写,也不会影响到其他参数的。
function test(x,y,z=1){
//z可以进行省略
}
function test(x,z=1,y){
//z无法进行省略,因为位置上会影响到后面的参数
}
触发默认参数
当传入undefined的时候才会触发默认值,而null和空字符串不会触发
function test(x=1){
console.log(x);
}
test(""); //""
test(null); //null
test(undefined) //1
函数length
使用了默认参数后,函数的length返回的是没有设置默认参数的参数lenght。
(function test(x,y){}).length //2
(function test(x,y=1){}).length //1
(function test(...arr){}).length //0
(function test(x,y=1,z){}).length //1
(function test(x=1,y,z){}).length //0
通过arguments
获取lenght并不会收到影响。
length的含义是这个函数预期传入的参数个数,当我们设置了默认值,那么这个预期就不一定了,所以不计入数量。
当我们把默认值设置在前面时,后面即便不是默认值也不会计入数量了。
作用域
一旦设置了默认值后,函数的参数在声明初始化的时候,会形成一个独立的作用域,等初始化结束,这个作用域就会消失,这种语法行为在不设置默认参数时,是不会出现的。
var x = 1;
function too(x,y = function(){ x=2 }) {
var x = 3;
y();
console.log(x);
};
too(); //3
console.log(x); //1
使用了默认参数,所以(x,y = function(){ x=2 })
参数会有独立的作用域。
其中y里面的x指向的是他同级作用域下的x,也就是第一个参数x。
而too函数里面,我们又申明了一个x并赋值为3,当y运行的时候,他的这个x指向的是他作用域下的第一个参数x,x赋值为2,而too里面的var x和第一个参数x不在同一个作用域。
所以y是无法修改到var x变量的,他只能修改第一个参数x。
所以:
function too(x,y = function(){ x=2 }) {
y();
console.log(x);
};
too(); //2
而我们运行too函数时,输出3,是因为,var x在第一个参数x后面声明的,就近原则,var x是最快获取到的,所以输出3,这里也可以了解下变量提升,而y这个函数,他本身也只能修改第一个参数,后面的var x他改不了,所以,最终结果是个3
如果我们不在too里面声明一个x,而是直接用参数x,那么结果又不同了
function too(x,y = function(){ x=2 }) {
x = 3;
y();
console.log(x);
};
too(); //2
这里就会输出为2,因为x是同一个x,同一个作用域。
应用
我们可以将默认值来指定某一个值是必填的,如果省略则进行报错
function err(name){
throw new Error(name+"参数为必填参数");
}
function test(x=err("x")){
return x;
}
test(); //报错:x参数为必填参数
另外我们可以给默认参数设置为undefined表示这个参数是可以省略的。
function test(option = undefined){
}
rest
rest用于处理特别多的参数的时候,以前我们用arguments对象获取所有参数,现在我们可以使用rest方法更加便捷的获取。
function add(...values) {
let sum = 0;
for(var val of values) {
sum += val;
}
return sum;
}
add(1,2,3); //6
通过...values
的方式,values成为了一个保存所有参数的数组,然后我们遍历这个数组,进行求和,然后返回结果。
values
这个变量名是可以自定义的。
例子:数字排序
//es5写法
function sortNumber() {
return Array.prototype.slice.call(arguments).sort();
}
//es6
const sortNumber = (...numbers) => numbers.sort();
例子:改写push
function push(arr,...items) {
items.forEach(funtion(item){
arr.push(item);
});
return arr;
};
push([],1,2,3,4); //[1,2,3,4]
注意
rest必须写在最后面,作为最后一个参数,否则报错,并且使用了rest后,lenght也无法检测到参数数量
function test(x,...vals,y){ ... } //报错
(function test(x,...vals){}).length; //1
(function test(...vals){}).length; //0
严格模式
es5开始,函数内部可以设定为严格模式
function test(){
'use strict'
...
}
es6做了点修改,如果函数使用了默认参数,解构赋值,rest扩展运算符,那么函数内部就不能显示设定为严格模式,否则就会报错。
但是可以通过一个方式规避:
全局严格模式
'use strict'
function test(){
...
}
函数包裹
const test = (function(){
'use strict'
return function(x = 1) {
...
}
})()
name
函数的name属性返回的该函数的函数名,但是在es5中,如果我们创建一个变量,并给这个变量赋值一个匿名函数,然后我们再来获取其name,得到的是空,而es6返回的是这个变量名
var f = function(){};
f.name; //es5 ""
f.name; //es6 "f";
如果被赋值这个这函数本身就是一个具名函数,那么name最终返回的是这个具名函数的名字。
var f = function test(){};
f.name; "test";
如果是Function构造函数返回的函数示例,返回的是:anonymous
(new Function).name //'anonymous'
如果是使用bind方法生成的新函数,会带有bound
前缀
function test() {};
test.bind({}).name; //'bound test'
(function(){}).bind({}).name; //bound
箭头函数
es6允许使用箭头来定义函数,箭头函数大大的简化的函数的书写,在一些回调上显得更加简洁明了。
var f = ()=>{};
//等同于
function f() {};
箭头函数还有一些省略写法:
var f1 = val => {}; //单参数
var f2 = (val1,val2) => {}; //多参数和无参数用圆括号
var f3 = val => val; //省略return
//等同于
funciton(val) {
return val;
}
var f4 => ({key:val}); //如果要return一个对象,可以用圆括号包裹用以区分
箭头函数本身也是函数,所以参数上,我们也能使用解构,rest扩展运算符,默认值,这里就不重复说明了。
箭头函数使得表达更加简洁,以前要写好几行的代码,现在可以缩短为一行
const isEven = n => n % 2 == 0;
const square = n => n * n;
注意事项
- this对象是函数定义时所在的对象,而不是使用时所在的对象
- 不可以当做构造函数使用,所以不能使用new
- 不可以使用arguments对象,可以使用rest参数代替
- 不可以使用yield命令,因此箭头函数不能作为generator函数
this对象是函数定义时所在的对象,而不是使用时所在的对象
function foo(){
setTimeout(()=>{
console.log('id:',this.id);
},100);
}
var id = 1;
foo.call({id:2}); //id:2
在foo函数内,有一个定时器,定时器的回调是一个箭头函数,按照第一条的说明,this对象是定义时所在的对象,也就是说this指向的是foo。
然后我们给全局window上定义了一个id变量,值为1;
然后我们将foo的this指向了一个对象,于是我们得到的是id:2
如果是普通函数,定时器里面的函数this指向的是window,按道理就要输出id:1
箭头函数可以让this指向固定化,这种特性非常有利于封装回调。
简单来讲,箭头函数没有自己的this,他的this永远是外层代码块的this,正因为没有this,所以不能用作构造函数。
像arguments、super、new.target这三个变量,在箭头函数里面也是没有的,但是可以获取到外层代码块的变量。
function foo(){
setTimeout(()=>{
console.log("arguments:",arguments);
},100)
}
foo(1,2,3,4); //arguments:,[1,2,3,4]
另外由于箭头函数没有自己的this,所以我们也不能改变箭头函数自己的this指向,所以call、apply、bind这些改变this指向的语法也无法使用。
绑定this
箭头函数绑定this对象,大大的减少了显示绑定this对象的写法,如:call、apply、bind,但是箭头函数并非适合所有场合,所以es7提出了“函数绑定”运算符,用来取代这三个方法,但是目前还是提案,不过bable转码器已经支持了。
函数绑定运算法是两个冒号:::
,双冒号左边是一个对象,右边是一个函数,改运算符会将左边的对象做为this的上下文环境。
foo::bar
//等同于
bar.bind(foo);
foo::bar(...arr)
//等同于
bar.apply(foo,arguments)
如果左边的为空,右边是一个对象的方法,那么就表示右边这个对象作为上下文。
::obj.foo;
//等同于
obj::obj.foo;
这样的话,双冒号返回的的还是原对象,类似于返回了一个this一样,可以拿来做链式写法。
var s = {
test:()=>{
console.log(1);
}
}
s.test()::test()::test()
这个是我自己想的,可能会有问题,也找不到地方测试。
尾部调用
指的是函数最后一步调用另一个函数,这个最后一步可以不是写在最后面,但他必须是函数最后一步。且无其他运算,如果你最后一步调用一个函数再操作,那就算不得。
function f(x) { //不算
let y = g(x);
return y;
};
function f(x) { //不算
return g(x) + 1;
};
function f(x) { //不算
g(x);
};
这里第一个和第二个不算,第三个的话,我们要明白他其实还要一步
function f(x) { //不算
g(x);
return undefined;
};
所以第三种也不算,他不是最后一步。
尾调用优化
尾调用之所以和其他不同,是因为js的函数运行,如果a函数里面运行了b函数,那么内存中a的运行区域上还有b函数,b如果有c,那么就会继续堆,从而形成一个调用栈,只有当c运行完了,c才会从内存中消失,然后运行b,所以,如果相关联的太多了,就会出现栈溢出。
尾部调用的话,说明他的这段函数已经不需要继续保存了,所以a会先消失,而不是要等后面所有的b、c函数运行完毕再消失。
如果是递归的话,a函数无限调用自身,传统的话太多就会直接溢出了,而使用尾部调用优化,那么就等于a运行完毕了,再开一个a,原来的消失,相对来说性能会更好。
如何实现优化呢
- 尾部调用的函数内部不要有外部的变量,可以用参数的形式传,但是不要直接在里面调用
- es6的尾调用优化只有在函数开启严格模式下才有效
为什么只能在严格模式下呢,是因为正常函数下,有两个变量可以跟踪函数的调用栈,导致原来的无法内存区域依旧保持存在。
- func.arguments 返回调用时函数的参数
- func.caller 返回调用当前函数的那个函数
而严格模式会禁用这两个变量,所以尾部优化只有在严格模式下有效
function test(){
"use strict"
test.caller; //报错
test.arguments; //报错
}
test();
尾调用优化实现
尾部优化只有在严格模式下有效,那么其他环境怎么办呢?
答案就是自己动手了,从原来上讲,优化的原理就是减少栈的堆叠,或者说是调用。
这是一个正常的递归函数:
function sum(x,y){
if(y>0){
return sum(x+1,y-1);
}else {
return x;
}
}
sum(1,100000); //报错,溢出
优化1:蹦床函数
//蹦床
function trampoline(f){
while(f && f instanceof Function) {
f = f();
}else {
return f;
}
}
//重复函数
function sum(x,y){
if(y>0){
return sum.bind(nyll,x+1,y-1);
}else {
return x;
}
}
trampoline(sum(1,100000)); //100001
蹦床函数并不是真正的尾部优化
尾部优化
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if(!active) {
active = true;
while(accumulated.length){
value = f.apply(this,accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x,y){
if( y > 0){
return sum(x+1,y-1);
}else {
return x;
}
});
sum(1,100000); //100001
函数参数的尾逗号
es7有一个提案,就是函数的参数,支持最后一个参数后面带一个逗号。这样可以使函数参数和数组参数的尾逗号规则保持一致。
function test(x,y,) {}; //现在这样写还是会报错的
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据