6. 函数的扩展
基本用法
在之前,我们不能为函数的参数指定默认的值,只能采用变通的方法
function Test(x){
x=x||'Tom';
}
//但是该写法不够直观,而且如果Test的函数的参数值为false,那么该赋值并不会起作用,所以,一般我们会判断参数x是否被赋值
function Test(x){
if(typeof x==undefined) x="Tom"
}
在ES6中允许我们为函数的参数设置默认值,也就是直接在参数后定义
function people(name="tom") {
this.name=name;
}
var Tom=new people();
console.log(Tom.name); //tom
//注意,有默认值的参数,不能在函数体中使用let,const再次声明,否则会报错,在使用参数默认值的时候,函数不能有同名参数
function Test(x,x,y){
} //正常
function Test(x=0,x,y){
} //报错
function Test(x=0,y){
let x=1;
}
//另外,参数默认值是不传值的,而是每次都重新计算默认值的表达式
//也就是说,参数默认值是惰性求值的
var x=99
function foo(y=x+1){
console.log(y);
x++;
}
foo(); //100
foo(); //101
//在上面的代码中,y的默认值是x+1,每次调用foo函数时都会重新计算x+1,而不是默认y=100
参数的默认值可以是一个函数
function foo(x=function(){...}){
}
与解构默认值结合使用
参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾部,因为这样比较容易看出来到底省略了哪些参数,如果非尾部的参数设置默认值,实际上该参数在传入时是无法省略的
function foo(x=1,y){
return [x,y]
}
foo() //1,undefined
foo(1,1)//1,1
foo(,1)//报错
foo(undefined,1)//1,1
我们可以看出实际上我们无法省略x来为y赋值,除非直接赋值为undefined,否则会报错
function foo(x=1.y=5){
return [x,y]
}
foo(undefined,null)//1,null
为已有默认值的函数参数赋值undefined能够触发函数的默认值,而null则不能,nul会直接赋值给参数
函数的length属性
指定了默认值以后,函数的length属性将返回没有设置默认值的参数的个数,也就是说函数的length属性将会失真,并且在设置了默认值之后的其它参数也不会被计入
function foo(a,b,c=3,d){
return [a,b,c,d]
}
console.log(foo.length) //2
作用域
唔,我觉得这一节叫做作用域链可能更贴切
函数参数在设置默认值之后,在预解析时会产生作用域,该作用域会确定改默认值的值
let x=1;
function foo(x,y=x){
console.log(y);
}
foo(2);//2
通过上面的例子我们可以看出,在默认值为变量时,在预解析时会优先去查找函数参数中是否有同名参数,,如果有的话会使用该参数的值,而不是全局变量的值
let x=1;
function foo(y=x){
let x=2;
console.log(y);
}
foo()//1
上例中在参数中不存在x的情况下,会去查找全局中可用的变量x,然后将其值赋值给y,而不是函数体中的x,如果我们将全局的x注释掉,那么文件会报错x is not defined
通过上面其实我们可以总结,如果在函数中存在设置默认值的参数,那么该参数会产生一个作用域,该作用域在函数的块级作用域和全局作用域之间,所以可以向上读取全局作用域的变量而不能向下读取函数体中的变量
如果参数的默认值是一个函数,该函数的作用域也遵循以上这个规则
let foo='outher'
function bar(func=()=>foo){
let foo="inner";
console.log(func())
}
bar() //outher
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
x // 1
//如果我们将函数体中的var x=3;这段代码注释掉,最后foo()的打印结果为2
通过以上实例我们可以总结,如果我们在一个函数中设置参数默认值,那么在预解析的时候会产生一个作用域,该作用域的作用域链的上端是全局函数,下端是函数体,有了这个认知,我们就可以去推断默认值的值
应用
我们可以利用默认值,指定某个参数不能被忽略,否则报错
function foo(a=(()=>{
throw new Error("参数a不能为空")
})() ){
console.log(a)
}
foo()//Uncaught Error: 参数a不能为空
上面的代码为a设置了一个自执行的函数为默认值,设置a的值为该函数执行后的返回值
我们也可以通过设置参数默认值为undefined,表明这个参数是可以省略的
function foo(a=undefined){
}
rest参数
ES6引入了rest参数(形式为...变量名),用于获取函数的多余参数
function foo(...rest){
console.log(rest);
}
foo(1,2,3) // [1, 2, 3]
在使用了rest语法后,我们可以通过rest来接收传入的多余参数,rest是一个数组,包含我们传入的多余部分的参数
需要注意的是,函数的length属性会忽略...rest参数,下面我们可以比较一下rest和argument的区别
function foo(num,...numbers) {
console.log(arguments)
console.log(numbers);
}
foo(1,2,3);
//[1, 2, 3, callee: (...), Symbol(Symbol.iterator): ƒ]
//[2, 3]
argument会返回所有传入的参数,而rest只会返回没有明确定义的参数,也就是我们所说的多余参数,argumengt是一个伪数组,不能使用数组方法,需要将其转化为数组对象才能使用,但是rest可以直接使用数组方法
Array.prototype.slice.call(argument).sort()
rest.sort()
所以在某些需要对参数进行数组操作的情况下,我们可以使用rest语法替代掉arguments
注意在...rest之后不能继续再添加参数,否则会报错
严格模式
在ES5开始,函数内部可以设定为严格模式
function doSomething(a,b){
'use strict'
//code
}
在ES6中规定,在使用了解构,默认值,或者拓展运算符的情况下不能在函数体内部设置显式的严格模式声明,否则会报错
function foo(...args){
'use strict'
}//报错
name属性
name属性早之前就已经被各大浏览器开始支持,并且我们之前也有使用过,但是是在ES6中才将其写入了标准,并且做了一些修改
name属性会返回该函数的函数名,并且之前我们如果讲一个匿名函数赋值给一个变量,那么该函数的name属性会返回空字符串,但是现在会返回该变量名称
var foo=function(){
//code
}
//ES5
foo.name //" "
//ES6
foo.name //"foo"
//如果是将一个具名函数赋值给变量,那么会返回该函数原本的名字
const bar=function foo(){ }
bar.name="foo"
箭头函数
ES6允许使用箭头(=>)定义函数
var f=v=>v;
//等同于
var f=function(v){
return v;
}
如果函数不需要参数或者需要多个参数,就使用一个圆括号代表参数部分
如果函数体内只有return语句那么我们可以在箭头后直接写该返回值,而不需要写括号
var f=()=> 5;
//等同于
var f=function(){
return 5;
}
var sum=(num1,num2)=>num1+num2;
//等同于
var sum=function(num1,num2){
return num1+num2;
}
如果箭头函数的代码块部分多于一条语句,那么需要用大括号将代码括起来
const f=(v)=>{
let a=v++;
if(a>2){
console.log(v)
}
}
加括号的函数体返回对象字面表达式:
args = > ( { foo : bar } )
由于大括号会被解析为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错
let getTempItem = id => { id: id, name: "Temp" }; //报错
let getTempItem = id => ({ id: id, name: "Temp" }); // 正确
箭头函数配合解构使用
let person={
first:1,
last:2
}
//es5
function full(person) {
console.log(person.first+person.last)
}
full(person);
//es6
const full=({first,last})=>{console.log(first+last)}
full(person);
箭头函数最大的好处就是简洁,例如我们简化一个回调函数
let arr=[1,2,3];
//es5
arr.map(function (v,i) {
console.log(v +"|" + i);
})
//es6
arr.map((v,i)=>{console.log(v+"|"+i)})
//配合rest参数使用
const numbers=(head,...tail)=>{console.log([head,tail])};
numbers(1,2,3,4)//[1,[2,3,4]]
使用注意点
箭头函数有几个使用注意点:
- 函数体内的this对象,就是第一是所在的对象,而不是使用时所在的对象
- 不可以当作构造函数使用,也就是所不可以通过箭头函数来new出一个对象,否则会报错
- 不可以使用arguments对象,该对象在函数体内不存在,如果要用可以使用rest参数代替
- 不可一世使用yield对象,所以箭头函数不能用作Generator 函数
需要特别注意,在箭头函数中this的指向是固定的,箭头函数的this指向是在函数定义生效时所在的对象,this指向的固定化并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,单子内部的this就是外层代码块的this,正是因为它没有this,所以不能用于构造函数
所以箭头函数转化成es5的代码是如下写法
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
由于箭头函数自身不存在this,所以也不存在使用call,appply,bind来改变箭头函数的this指向的问题
箭头函数的this指向固定的特性很有利于代码的封装回调函数
const header={
id:12306,
init:function () {
document.addEventListener('click',
event=>this.doSomething(event.type,false),
)
},
doSomething:function (type) {
console.log('Handing'+type+'for'+this.id);
}
}
在es5中如果我们写上面的代码一定需要注意的是设置一个变量来接收header的this,然后在init中使用该变量来替代this,因为在运行时init方法中的this指向的不是header而是document,但是使用箭头函数就不需要考虑这个问题,箭头函数的this是一定指向header的
function Timer() {
this.s1 = 0;
this.s2 = 0;
setInterval(() => {
this.s1++;
}, 1000);
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => {
console.log('s1' + "|" + timer.s1);//s1|2
}, 2001);
setTimeout(() => {
console.log('s2' + "|" + timer.s2);//s2|0
}, 2001);
我们在计时器中使用了两种不同的写法,最后打印的结果s1变为2,s2没有变化,因为在箭头函数在函数生效时查找的this.s1是到Timer下查找,此时s1存在并且值为0,而匿名函数是在window下查找,而window并没有定义s2这一属性
长期以来,JS的this对象一直是个令人很头痛的问题,在对象方法中使用this一直需要注意当前的this是不是指向当前的对象,而箭头函数很大程度上可以解决这个问题