函数
参数默认值,以及一些关于arguments对象,如何使用表达式作为参数、参数的临时死区。
以前设置默认值总是利用在含有逻辑或操作符的表达式中,前一个值是false时,总是返回后面一个的值
,但如果我们给参数传0时,就有些麻烦。需要去验证一下类型
function makeRequest(url,timeout,callback){
timeout = timeout || 2000;
callback = callback || function(){};
// 逻辑
}
// 解决传0问题
function makeRequest(url,timeout,callback){
timeout = typeof timeout !=='undefined'?timeout: 2000;
callback = typeof callback !=='undefined'?callback : function(){};
// 逻辑
}
默认参数
不传或传undefined
。如果传null
不会用默认,会认为是一个合法的值。默认参数对arguments对象的影响。之前的参数传递会体现在arguments对象上。 在es5非严格模式下,命名参数的变化会同步更新到arguments对象上,但在es5的严格模式下,取消了这个行为,无论参数如何变化,arguments对象不再随之改变。 如下
function mixArgs(first,second){
"use strict"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = 'c';
second = 'd'
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs('a','b')
// true
// true
// false
// false
es6中,如果一个函数使用了默认参数值,则无论是否显式定义了严格模式,arguments对象的行为都将与es5 严格模式下保持一致。
默认参数表达式
初次解析函数声明时不会调用getValue()方法,只有当调用add()函数且不传入第二个参数时才会调用。
let value = 5;
function getValue(){
return value++;
}
function add(first,second=getValue()){
return first+second;
})
console.log(add(1,1)) // 2
console.log(add(1)) // 6
console.log(add(1)) //7
先定义参数可以作为后定义参数的默认值,反过来不成立(first=second,second)因为second比first晚定义,因此其不能作为first的默认值。为了帮助你理解背后的原理,我们重温一下临时死区TDZ的概念。与let声明类似,定义参数时会为每个参数创建一个新的标识符绑定,该绑定在初始化之前不可被引用,如果试图访问会导致程序抛出错误。
函数参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内的变量
无论是是否使用不定参数,arguments对象总是包含所有传入函数的参数
处理无命名参数--不定参数
// 函数的length属性统计的是函数命名参数的数量,不定参数的加入不会影响length属性的值。
function pick(object, ...keys){
let result = Object.create(null);
for(let i=0,len=keys.length;i<len;i++){
result[keys[i]] = object[keys[i]]
}
return result;
}
- 不定参数(当然在函数参数中使用)
- 每个函数最多只能声明一个不定参数
- 而且一定要放在所有参数的未尾
- 不许在setter中使用不定参数,是因为对象字面量setter的参数有且只能有一个(在不定参数的定义中,参数的数量可以无限多,所以在当前上下文中允许使用不定参数。
let object = {
// 语法错误 不可以在Setter中使用不定参数
set name(...value){
// ...
}
}
无论是否使用不定参数,arguments对象总是包含所有传入函数的参数
增强的Function构造函数
可以在动态创建函数时使用默认值及不定参数,唯一需要做的是在参数名后添加一个等号和一个默认值,定义不定参数,只需在最后一个参数前添加...
。如下
var add = new Function('first','second=first','return first+second')
console.log(add(1,1)) // 2
console.log(add(1)) // 2
var pickFirst = new Function("...args",'return args[0]')
console.log(pickFirst(1,2)) // 1
展开运算符
展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数。JavaScript内建的Math.max()
方法可以接受任意数量的参数并返回值最大的那个,这是一个简单的用例
自己理解,注意,前面那个是定义时用的,这个是调用时使用(参数是分开的,而你有一个数组)
如下,之前数组求最大值的做法
let values = [25,50,75,100]
console.log(Math.max.apply(Math,values)) // 100;
// max方法不允许传入一个数组
console.log(Math.max(...values))
// 展开运算符后面可以继续添加参数
console.log(Math.max(...values,110)) //110
函数的name属性
声明函数.name
属性是它声明函数名,函数表达式如果函数是匿名的则就是其变量的名称,如果不是则是其声明的名字,
它只是协助调试用的额外信息,所以不能使用name属性的值来获取对于函数的引用。
function somethingDoing(){
// 空函数
}
var doSomeThing = function doSomethingElse() {
// 空函数
}
var person = {
get firstName(){
return 'Nicholas';
},
sayName:function(){
console.log(this.name)
}
}
console.log(somethingDoing.name) // somethingDoing
console.log(doSomeThing.name) // doSomethingElse 函数表达式有个名字这个名字比要赋值的变量权重高
console.log(person.sayName.name) // sayName
console.log(person.firstName.name) // get firstName
// 两个特例
// 通过bind()创建的函数 会带前缀bind
var doSomthing = function(){}
console.log(doSomthing.bind().name) // "bound doSomething"
console.log(new Function().name) // anonymous
明确函数的多重用途
es5 当new 时 函数内的this值指向一个新对象,函数最终返回一个新对象(没有明确返回一个对象时)
function Person(name){
this.name = name;
}
var person = new Person('Nicholas');
var notPerson = Person('Nicholas');
console.log(person) // '[Object object]'
console.log(notPerson) // undefined
上例中,在es6中函数有两个不同的内部访问[[Call]]
和[[Construct]]
。当通过new关键字调用函数时,执行的是[[Construct]]
函数,它负责创建一个通常被称作实例的新对象,然后执行函数体,将this绑到实例上。而不使用new关键字时,则调用[[Call]]
执行函数体,具有[[Construct]]
方法的函数统称为构造函数。切记不是所有的函数都有[[Construct]]
方法 如:箭头函数
es5判断函数是否被new调用
function Person(name){
if(this instanceof Person){
this.name = name
}else {
throw new Error('必须通过new关键字来调用Person')
}
}
上面的方式在一定程度上可以避免,但不要忘了还有一个call和apply呢……
var person = new Person('Nicholas');
var person = Person('Nicholas'); // 报错
var person = Person.call(person,'Michael'); // 通过
无属性new.target
为了解决判断函数是否通过new
关键字调用的问题,es6引入了new.target这个元属性,元属性是指非对象的属性,其可以提供非对象目标的补充信息。当调用函数[[Construct]]
方法时,new.target上被赋为new的操作目标,通常是新创建对象实例,也就是函数体内this的构造函数;如果调用[[Call]]
方法,则new.target值为undefined
,有了这个元属性,可以通过检查new.target
是否被定义过来安全地检测是否通过new关键字调用的。
function Person(name){
if(typeof new.target!=='undefined'){
this.name = name;
}else {
throw new Error('必须通过new关键字来调用Person')
}
}
var person = new Person('Nicholas'); // 通过
var person = Person('Nicholas'); // 报错
var person = Person.call(person,'Michael'); // 报错
也可能通过new.target
是否被某个特定的构造函数所调用。如下:
function Person(name){
// if(typeof new.target!=='undefined'){
if(new.target===Person)
this.name = name;
}else {
throw new Error('必须通过new关键字来调用Person')
}
}
function AnotherPerson(name){
Person.call(this,name)
}
var person = new Person('Nicholas');
var otherPerson = new AnotherPerson('Michael') // 抛出错误
块级函数
es5的严格格式下,我们在代码块中声明函数是不被允许的,而在es6中将doSomething()函数视作一个块级声明,从而可以在定义该函数的代码内访问和调用它:
"use strict"
if(true){
console.log(typeof doSomething) // "function"
function doSomething(){
// 空函数
}
doSomething() //
}
typeof doSomething // undefined
和let的使用差不多,唯一区别的是块级是函数,会将声明提到代码块的顶部。注意函数表达式声明的区别。在es6中,即使处于非严格模式下,也可以声明块级函数,但其行为与严格模式下稍有不同。这些函数不再提升至代码块的顶部,而是提升至外围函数或全局作用域的顶部,如下:
// ECMAScript 6中的行为
if(true){
console.log(typeof doSomething) // "function"
function doSomething(){
// 空函数
}
doSomething() //
}
typeof doSomething // function
箭头函数
let PageHandler = {
id:"123456",
init: function(){
document.addEventListener("click",function(event){
this.doSomething(event.type); // 抛出错误
},false)
},
doSomething: function(type){
console.log("Handling "+ type + " for "+this.id)
}
}
上面的代码并没有如预期的正常运行。因为this的绑定是事件目标对象的引用(这里是document),而没有绑定在PageHandler
,且由于this.doSomething()在目标document中不存在,所以无法正常执行,之前的做法是通过bind()
方法显示地将this
绑定到PageHandler
函数上来修正这个问题。如下
let PageHandler = {
id:"123456",
init: function(){
document.addEventListener("click",(function(event){
this.doSomething(event.type);
}).bind(this),false)
},
doSomething: function(type){
console.log("Handling "+ type + " for "+this.id)
}
}
调用bind(this)
后事实上创建了一个新函数,它的this被绑定到当前的this即PageHandler。为了避免创建一个额外的函数,我们可以通过一个更好的方式来修正这段代码,使用箭头函数。箭头函数中没有this绑定,必须通过查找作用域链来决定其值。
let PageHandler = {
id:"123456",
init: function(){
document.addEventListener("click",(event)=>{
this.doSomething(event.type);
},false)
},
doSomething: function(type){
console.log("Handling "+ type + " for "+this.id)
}
}
- 没有this、super、arguments和new.target绑定
- 不能通过new关键字调用 箭头函数没有
[[Construct]]
方法,所以不能被用作构造函数 - 没有原型
- 不可以改变this的绑定
- 不支持arguments对象
- 不支持重复的命名参数
箭头函数也同样有一个name属性,这与其它函数的规则相同
this绑定
如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;否则,this的值会被设置为undefined.
箭头函数和数组
箭头函数的语法简洁,非常适用于数组处理。举例来说,比如给数组排序,通过是
var result = values.sort(function(a,b){
return a - b;
})
// 我们可以将其简写至如下
var result = values.sort((a,b)=>a-b)
诸如
sort()
、map()
、reduce()
这些可以接受回调函数的数组方法,
尾调用优化
函数做为另一个函数的最后一条语句被调用。
function doSomething(){
return doSomethingElse() // 尾调用;
}
在es5引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧(stack frame),将其推入调用栈来表示函数调用。也就是说,在循环调用中,每一个未用完的栈桢都会被保存在内存中,当调用栈变得过大时会造成程序问题。
为了缩减严格模式下尾调用栈的大小(非严格格式不受影响),如果满足以下条件,尾调用不再创建新的栈桢,而是清除非重用当前栈桢
- 尾调用不访问当前栈桢的变量(也就是说函数不是一个闭包)
- 在函数内部,尾调用是最后一条语句
- 尾调用的结果作为函数的返回值
"use strict"
function doSomething(){
return doSomethingElse() // 尾调用优化;
}
// 注意以上3个条件
如何利用尾调用优化
function factorial(n){
if(n<=1){
return 1
} else {
// 无法优化
return n * factorial(n-1)
}
}
// 可以利用传递第二个参数来保存每次阶乘的结果(并且默认结果是1)
function factorial(n,p=1){
if(n<=1){
return 1*p
}else {
let result = n*p;
//优化后
return factorial(n-1,result)
}
}
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://joyjs.cn/archives/4310