函数拓展
1.参数变量是默认声明的,所以不能用let
或const
再次声明。
function a(x = 5){
let x = 1; //error
}
2.使用参数默认值时,函数不能有同名参数。
function a(x,x,y){} //正确
function a(x,x,y=1){} //错误
3.参数默认值不是传值的,而是每次都重新计算默认值表达式的值。
let a = 90;
function x( p = b + 1){
console.log(p);
}
x() //91
a = 100;
x() //101
每次调用函数x,都会重新计算b + 1
与解构赋值默认值结合使用
1.参数默认值与解构赋值并用,可以避免因未传入参数导致的不生成变量报错状况
//只使用解构赋值,未定默认值:
function foo({x, y = 5}) {
console.log(x, y);
}
foo() // TypeError: Cannot read property 'x' of undefined
//默认值 + 解构赋值结合
function foo({x,y=5} = {}){
console.log(x,y);
}
foo() //undefined 5
2.如果设置默认值是一个具体对象:
function a({x, y} = { x: 0, y: 0 })
代表:默认值是一个有属性的对象,并未设置解构赋值,所以需要按照该默认值对象个数进行传值
- 传入空对象时没有值的显示:[undefined,undefined]
- 传入参数个数不相同时:a({x:1}) [1,undefined]
- 传入参数不存在时:[undefined,undefined]
参数默认值的位置
1.参数默认值应该设在尾部,如果是非尾部的位置,则这个参数无法省略,想省略可以传入undefined,null不可以。
函数的 length 属性
1.指定默认值后,lenth属性将失真(从指定默认值的参数位置开始往后所有的参数,将不计入length)。
作用域
1.一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域,初始化结束后作用域会消失。
let x = 1;
function f(x,y = x){
console.log(x);
}
f(2) //2 默认值变量x指向第一个参数x,而不是全局变量x
2.默认值中的参数未定义,由於单独作用域原因会去全局中找该参数,故函数内部无法影响该参数,如果全局中不存在该参数,则报错。
let a = 10;
function f(x = a){
let a = 2;
console.log(x);
}
f() //10
function f2(x = b){
let b = 2;
console.log(x);
}
f2() //error
3.暂时性死区的状况:(由於单独作用域原因,x=x实际执行的是let x = x)
let x = 1;
function f(x = x){
...
}
f(); //x is not defined
4.默认值是函数的时候同样如此
5.复杂示例
var x = 1;
function foo( x , y = function (x){ x = 2;} ){
var x= 3;
y();
console.log(x);
}
foo();//3
x ;//1
解析:参数为单独作用域,匿名函数y与参数x在同一作用域,所以不影响全局中的x,函数中单独定义了x,由于作用域不同,所以匿名函
数y并不能影响函数内部x,故调用函数时输出3,全局中x还为1。
6.当去掉var:
var x = 1;
function foo( x , y = function (x){ x = 2;} ){
x= 3;
y();
console.log(x);
}
foo();//2
x ;//1
去掉var后,函数内x指向参数x,故匿名y的执行会影响函数内部,所以输出2,由于作用域不同,全局仍不受影响。
7.应用--设置一个函数必须传参才能调用:
function a(){
throw new Errow('请传入参数再调用')
}
function foo(b = a()){
return b;
}
foo(); //Error:请传入参数再调用
8.参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。
9.将参数默认值设为undefined
,表明这个参数是可以省略的。
Rest参数
1. rest 参数(形式为...变量名
),用于获取函数的多余参数,rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
2.是一个真正的数组,数组特有的方法都可以使用。
3.rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
4.函数的length
属性,不包括 rest 参数。
严格模式
1.只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
2.两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。
3.第二种是把函数包在一个无参数的立即执行函数里面。
name属性
1.函数的name
属性,返回该函数的函数名。
2.如果将一个匿名函数赋值给一个变量,ES5 的name
属性,会返回空字符串,而 ES6 的name
属性会返回实际的函数名。
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
3.Function
构造函数返回的函数实例,name
属性的值为anonymous
。
4.bind
返回的函数,name
属性值会加上bound
前缀。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
箭头函数
1.由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
let getTempItem = id => ({ id: id, name: "Temp" });
下面是一种特殊情况,虽然可以运行,但会得到错误的结果。
let foo = () => { a: 1 };
foo() // undefined
//a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。
2.箭头函数有几个使用注意点:
(1)函数体内的this
对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其值得注意。this
对象的指向是可变的,但是在箭头函数中,它是固定的。
3.this
指向的固定化,并不是因为箭头函数内部有绑定this
的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
4.由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。
5.除了this
,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments
、super
、new.target
。
6.不适应场景:
- 第一个场合是定义函数的方法,且该方法内部包括
this,调用函数时,this指向了全局对象,因此不会得到预期结果。
let cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
cat.jumps(); //NaN 全局中并没有定义lives
- 第二个场合是需要动态
this
的时候,也不应使用箭头函数。
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
this -> Window
因为button
的监听函数是一个箭头函数,导致里面的this
就是全局对象。
动态this -- 动态绑定事件 -- xx.addEventListener
- 如果函数体很复杂,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。
部署管道机制(pipeline)的例子详解:
在解析之前需要的知识点:
1.reduce()方法:
按照菜鸟教程所说:
语法和参数设定:
接下来看原函数:
const pipeline = (...funcs) =>
val => funcs.reduce((a, b) => b(a), val);
const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = pipeline(plus1, mult2);
addThenMult(5)
看起来并不好理解,用ES5转换一下:
const pipeline = function (...funcs) {
return function (val){
return funcs.reduce(
function(a,b){
return b(a);
},val);
}
}
const plus1 = function (a){
return a + 1;
}
const mult2 = function (a){
return a * 2;
}
这样看起来会清楚一些,部署管道机制意思就是将上一个函数的输出传递给下一个函数使用:
我们来输出一下addThenMult(它是上一个pipeline的输出):
//当将参数传入pipeline调用后,我们输出一下:
ƒ (val) {
return funcs.reduce(
function (a, b) {
return b(a)
}, val
)
}
这个时候pipeline的参数(两个function)和上面的输出即为addThenMult所用,然后执行addThenMult(5)
由于该示例非常具有误导性(可能是我脑子不太好使= =),所以分布写出来:
//步骤1:
ƒ (5) {
return funcs.reduce(
function (a=>a+1(total初始值), a=>a*2) {
return b(a)
}, 5 //传递给初始值
)
}
//步骤2:
ƒ (5) {
return funcs.reduce(
function (a=>5+1(total初始值), a=>a*2) {
return b(a)
}, 5 //传递给初始值
)
}
//步骤3:
ƒ (5) {
return funcs.reduce(
function (a=>5+1(total初始值), a=>a*2) {
return b(6)
}, 5 //传递给初始值
)
}
//步骤4:
ƒ (5) {
return funcs.reduce(
function (a=>5+1(total初始值), a=>a*2) {
return 6=>6 * 2 //12
}, 5 //传递给初始值
)
}
可读性强的写法:
const plus1 = a => a + 1;
const mult2 = a => a * 2;
mult2(plus1(5))// 12
//ES5
const plus1 = function(a){
return a+1;
}
const mult2 = function(a){
return a*2;
}
步骤:
- plus1先传入5调用 返回 5+1为mult2所用
- mult2使用6作为参数带入调用,返回12
双冒号运算符
Emmm,到目前应该还是个提案
尾调用优化
1.某个函数的最后一步是调用另一个函数为尾调用。
2.当最后一步调用函数后还有操作时不叫尾调用,没有最后一个return确认尾部时也不属于尾调用。
// 情况一
function f(x){
let y = g(x);
return y;
}
// 情况二
function f(x){
return g(x) + 1;
}
// 情况三
function f(x){
g(x);
}
3.尾调用不一定出现在函数尾部,只要是最后一步操作即可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
//都属于尾调用 都是函数f的最后一步操作
4.函数调用会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内部变量等信息,如果A调B,则在A帧上方会形成一个B帧,B运
行结束后返回结果B帧才会消失,以此类推会形成调用栈。
这个过程的优化叫做尾调用优化,即只保留内层函数的调用帧。
function g(x){
return x;
}
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
1.只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
function addOne(a){
var one = 1;
function inner(b){
return b + one; //用到了外层one , 所以不会进行优化
}
return inner(a);
}
尾递归
1.递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)
2.尾递归,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
下面是例子:
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面函数会保存很多调用记录,改写:
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120 total的默认值为1
斐波那契数列:
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
//尾递归优化过的 Fibonacci 数列实现如下。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
/*
Fibonacci2(3)的调用过程
return Fibonacci2(2,1,2)
return Fibonacci2(1,2,3) n<=1 输出3
*/
递归函数的改写
.....