函数
函数形参的默认值
在ES5中模拟默认参数
- 第一种方式:
- 缺陷: 如果给num传入值为0, 那么因为被视为false,所以num在函数内为100。
function aa(num, callback) {
num = num || 100;
callback = callback = function() {};
}
- 第二种方式:
- 常见于流行的JS库中
function aa(num, callback) {
num = (typeof num !== "undefined") ? num : 100;
callback = (typeof callback !== "undefined") ? callback : function() {};
}
ES6中的默认参数
- 提供一个初始值
- 例如:
function aa(num = 100, callback = function() {}) {
// ...
}
- 声明函数时,可以为任意参数设置默认值
- 在已指定默认值的参数后可以继续声明无默认值的参数
- 如:
function aa(num = 100, callback) {}
- 此时,只有当 不为第二个参数传值 或 主动为第二个参数传入“undefined”时,第二个参数才会使用默认值
- 传入null,不会使用第二个参数的默认值,其值最终为null
- 如:
默认参数对arguments对象的影响
- ES5中 非严格模式下, 命名参数的变化会被同步更新到arguments对象中
- ES5的严格模式下,无论参数在函数体内如何变化,都不会影响到arguments对象
- ES6中,如果一个函数使用了默认参数,那么如论何种模式下,都与严格模式保持一致
- 默认参数的存在使得arguments对象与命名参数保持分离
function aa(x, y ="y") {
console.log(arguments.length);
console.log(x === arguments[0]);
console.log(y === arguments[1]);
x = "x";
y = "000"
console.log(x === arguments[0]);
console.log(y === arguments[1]);
}
+ 输出结果为:```1, true, false, false, false```
+ 这种特性可以让我们通过arguments对象将参数回复为初始值
默认参数表达式
- 非原始值传参
- 只有调用add()函数且不传入第二个参数时才会调用getValue()
function getValue() {
return 5
}
function add(x, y = getValue()) {
return x + y;
}
console.log(add(1, 1));
// 2
console.log(add(1));
// 6
+ 当使用函数调用结果作为默认参数值时,如果忘记写小括号,最终传入的是函数的引用,而不是函数调用的结果
- 可以使用先定义的参数作为后定义参数的默认值,但不能用后定义的参数作为先定义参数的默认值
- 临时死区(TDZ)
function add(x = y, y) {
return x + y;
}
console.log(add(1, 1));
// 2
console.log(add(1));
// 抛出错误
// 表示调用add(undefined, 1), 即:
// let x = y;
// let y = 1;
// 此时会报错
默认参数的临时死区
- 与let声明类似
- 定义参数时会为每个参数创建一个新的标识符绑定
- 该绑定在初始化之前不可被引用
- 如果试图访问,会导致程序抛出错误
处理无命名参数
- 无论函数已定义的命名参数有多少,调用时都可以传入任意数量的参数
- 当传入更少的参数时,默认参数值的特性可以有效简化函数声明的代码
- 当传入更多数量的参数时,需要用到以下ES6的新特性
ES5中的无命名参数
- 实例:返回一个给定对象的副本,包含院士对象属性的特定子集
- 模仿了Underscore.js中的pick()方法
function pick(obj) {
let result = Object.create(null);
// 从第二个参数开始
for (let i = 1, len = arguments.length; i < len; i++) {
result[arguments[i]] = obj[arguments[i]];
}
return result;
}
let book = {
author: "shaun",
age: 20
};
let data = pick(book, "author", "age");
// shaun
console.log(data.author);
// 20
console.log(data.age);
- 不足:
- 不容易发现这个函数可以接受任意数量的参数
- 因为第一个参数为命名参数且已经被占用,当需要查找需要拷贝的属性名称时,需要从索引1开始遍历arguments对象
- 可以用ES6的不定参数特性解决
不定参数
- 在函数的命名参数前加三个点(...)就表明这是一个不定参数
- 该参数为一个数组,包含"自它之后"传入的所有参数
- 这个特性使得可以放心遍历keys对象了,没有索引的特殊性
- 另一个好处是只需看一眼,就能知道函数可以处理的参数数量
- 通过这个数组名,即可访问到里面的参数
- 重写pick函数
function pick(obj, ...keys) {
let result = Object.create(null);
for (let i = 1, len = arguments.length; i < len; i++) {
result[keys[i]] = obj[keys[i]];
}
return result;
}
- 不定参数的使用限制
- 每个函数只能声明一个不定参数,而且只能放在所有参数的末尾
- 不定参数不能用于对象字面量stter之中
- 因为setter的参数有且只能有一个
let obj = {
// 会报错,不可以在setter中使用不定参数
set name(...value) {
// to do...
}
}
- 不定参数对arguments的影响
- 无论是否使用不定参数,当函数被调用时,arguments对象依然包含了所有传入的参数
function check(...args) {
console.log(args.length);
console.log(arguments.length);
console.log(args.[0], arguments[0]);
console.log(args.[1], arguments[1]);
}
check("a", "b");
// 输出:
// 2
// 2
// a a
// b b
增强的Function构造函数
- Function构造函数是来动态创建新函数的方法
- 接受字符串形式的参数作为函数的参数和函数体(最后一项默认为函数体)
var add = new Function("x", "y", "return x + y");
// 2
console.log(add(1, 1));
- ES6中支持定义默认参数和不定参数
- 默认参数
var add = new Function("x", "y = 2", "return x + y");
// 2
console.log(add(1, 1));
// 3
console.log(add(1));
+ 不定参数(只能在最后一个参数前加...)
var pick = new Function("...args", "return args[0]");
// 1
console.log(pick(1, 2));
展开运算符
- 与不定参数的区别
- 不定参数可以让你指定多个各自独立的参数,并通过数组来访问
- 展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数
- ES5 实例
let value = [25, 50, 75, 100];
// 100
console.log(Math.max.apply(Math, value));
- ES6 实例
let value = [25, 50, 75, 100];
// 等价于
// console.log(Math.max(25, 50, 75, 100));
// 100
console.log(Math.max(...value));
- 可以将展开运算符与其他正常传入的参数混合使用
let value = [25, -50, -75, -100];
let value2 = [-25, -50, -75, -100];
// 25
console.log(Math.max(...value, 0));
// 0
console.log(Math.max(...value2, 0));
- 大多数使用apply()方法的情况下,展开运算符都可能是一个更适合的方案
name属性
- 辨别函数对调试和追踪难以解读的栈记录
- ES6 为所有函数新增了name属性
如何选择合适的名称
- name属性都有一个合适的值
function dosth() {
//
}
var doelse function() {
//
};
// dosth
dosth.name;
// doelse
doelse.name;
name属性的特殊情况
- 特殊情况
var dosth = function doelse() {
//
};
var person = {
get firstName() {
return "shaun";
},
sayName: function() {
console.log(this.name);
}
}
// 由于函数表达式的名字比函数本身被赋值的变量的权重高
// doelse
dosth.name;
// 取值对象字面量
// sayName
person.sayName.name;
// getter和setter,都会有前缀
// get firstName
person.firstName.name;
+ 其他两种前缀
var dosth = function() {
//
};
// bound dosth
dosth.bind().name;
// anonymous
(new Function()).name;
- 函数的name属性的值不一定引用同名变量,只是协助调试用的额外信息
- 所以不能使用name属性的值来获取对于函数的引用
明确函数的多重用途
- 当通过new关键字调用函数时,执行的是[[construct]]函数
- 负责创建一个实例新对象
- 然后再执行函数体
- 将this绑定到实例上
- 不通过new调用时,执行的是[[call]]函数
- 直接执行函数体
- 具有[[construct]]方法的函数被统称为构造函数
- 没有[[construct]]方法的函数不能通过new来调用
在ES5中判断函数被调用的方法
- ES5 确定一个函数是否通过new关键字被调用,最流行用instanceof
- 缺点:不完全可靠,例如call
function person() {
if (this instanceof person) {
this.name = name;
} else {
throw new Error("msg")
}
}
// 成功执行
var person = new Preson("shaun");
// 抛错
var notPerson = Preson("shaun");
// 也被成功执行
var notPerson2 = Preson.call(person, "shaun");
元属性(Metaproperty) new.target
- 此特性可以解决判断是否通过new调用的问题
- 当调用函数的[[construct]]方法时,new.target被赋值为new操作符的目标
- 当调用函数的[[call]]方法时,new.target被赋值为undefined
- 在函数体外使用new.target是一个语法错误
块级函数
- ES5 中不能在例如if语句中创建函数
- ES6 中可以,但是只能在代码块中调用此函数,代码块结束执行后,此函数将不再存在
块级函数的使用场景
- 严格模式下
- let 声明的变量不会提升到代码块顶部
- 声明的函数会提升到代码块顶部
- 非严格模式下
- 函数直接提升到外围函数或全局作用域的顶部
箭头函数
- 没有this, super, arguments, new.target的绑定
- 这些值由外围最近一层非箭头函数决定
- 不能通过new调用
- 因为没有[[construct]]方法
- 没有原型
- 不可以改变this的绑定
- 在函数生命周期内都不会变
- 不支持重复的命名参数
- 也有一个name属性
箭头函数语法
- 传入一个参数
let fn = val => val;
- 传入2个以上的参数
let sum = (x, y) => x + y;
- 不传参数
let name = () => "shaun";
- 由多个表达式组成的函数体要用{}包裹,并显式定义一个返回值
let sum = (x, y) => { return x + y; };
- 除了arguments对象不可用外,某种程度上都可以将花括号内的代码视作传统的函数体
- 创建一个空函数,仍需要写一对没有内容的花括号
- 如果想在箭头函数外返回一个对象字面量,需要将该对象字面量包裹在小括号内(为了将其与函数体区分开)
let getId = id => ({ id: id, name: "shaun" });
// 相当于
let getId = function(id) {
return {
id: id,
name: "shaun"
};
};
创建立即执行函数表达式
- 函数的一个流行使用方式是创建IIFE
- 定义一个匿名函数并立即调用
- 自始至终不保存对该函数的引用
- 可以用作创建一个与其他程序隔离的作用域
let person = function(name) {
return {
getName: function() {
return name;
}
};
}("shaun");
// shaun
console.log( person.getName());
- 将箭头函数包裹在小括号内,但不把("shaun")包裹在内
let person = ((name) => {
return {
getName: function() {
return name;
}
};
})("shaun");
// shaun
console.log( person.getName());
箭头函数没有this绑定
- 箭头函数的this值取决于该函数外部非箭头函数的this值
- 箭头函数内没有this绑定,需要通过查找作用域链来决定其值
- 否则会被设置为undefined
箭头函数和数组
- 箭头函数适用于数组处理
- 实例:排序
var value = [1, 2, 3, 9, 8];
// 老办法
var result = value.sort(function(a, b) {
return a - b;
})
// 新办法
var result2 = value.sort((a, b) => a - b);
- 诸如sort(), map(), reduce()这些可以接受回掉函数的数组办法
- 都可以通过箭头函数语法简化编码过程,减少编码量
箭头函数没有argument绑定
- 箭头函数没有自己的arguments对象
- 但是无论在哪个上下文中执行,箭头函数始终可以访问外围函数的arguments对象
箭头函数的辨识方法
- 同样可以辨识出来
var result = (a, b) => a - b;
// function
console.log(typeof result);
// true
console.log(result instanceof Function);
- 仍然可以在箭头函数上调用call(), apply(), bind()方法,但有区别:
var result = (a, b) => a + b;
// 3
console.log(result.call(null, 1, 2));
// 3
console.log(result.call(null, 1, 2));
var boundResult = result.bind(null, 1, 2);
// bind后的方法不用传参
// 3
console.log(boundResult());
尾调用优化
- 尾调用指的是函数作为另一个函数的最后一条语句被调用
- 如此会创建一个新的栈帧:stack frame
- 在循环调用中,每一个未用完的栈帧都会被保存在内存中
function dosth() {
// 尾调用
return doelse();
}
ES6中的尾调用优化
- ES6 缩减了严格模式下尾调用栈的大小
- 非严格模式下不受影响
- 满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧
- 尾调用不访问当前栈帧的变量(即函数不是闭包)
- 在函数内部,尾调用是最后一条语句
- 伟嗲用的结果作为函数值返回
如何利用尾调用优化
- 递归函数是其应用最常见的场景,尾调用优化效果最显著
function fn(n) {
if (n <= 1) {
return 1;
} else {
return n * fn(n - 1);
}
}
上面的代码中,递归调用前执行了乘法,因而当前版本的阶乘函数不能被引擎优化
当n是一个非常大的数时,调用栈的尺寸就会不断增长并存在最终导致溢出的风险
-
优化方法:
- 首先确保乘法不在函数调用后执行
- 这里使用默认参数来将乘法移除return语句
- 首先确保乘法不在函数调用后执行
function fn(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
// 优化后
return fn(n - 1, result);
}
}