ECMAScript 6的一些注意点 第二部分(函数拓展)

函数拓展

1.参数变量是默认声明的,所以不能用letconst再次声明。

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,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.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)    //输出 ac2 = 3  


*/

递归函数的改写

1.尾递归的实现缺点是不太直观,第一眼很难看出来

解决方案:

一、递归外再定义一个函数

function foo( n , total){
    if(n === 1) return total;
    return foo(n-1,n*total)
}
//正常形式函数,调用它即可
function factorial(n){
    return foo(n,1);
}
factorial(5)

柯里化(意思是将多参数的函数转换成单参数的形式),先了解一下JavaScript中的call()方法,MDN上:

1.call()允许为不同的对象分配和调用属于一个对象的函数/方法

2.call()提供新的 this 值给当前调用的函数/方法。你可以使用call来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)

3.call() 方法调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。

注意:该方法的作用和 apply() 方法类似,只有一个区别,就是call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组

然后我们参照JavaScript高级程序设计中的示例来解释call()和this:

1.函数内的一个特殊对象是this,引用的是函数据以执行的环境对象(当在全局作用域中调用函数时,this对象引用的就是window)

window.color  = 'red';
var o = { color:"blue" };
function sayColor(){
    console.log(this.color);
}
sayColor();    //red
o.sayColor = sayColor;        //发生了耦合
o.sayColor();    //blue

解析:

sayColor是在全局函数中定义的,他引用了this对象,调用之前this并不确定,在全局调用时,this指向window,将函数赋值给o并调用,则

this引用对象o,因此对this.color求值将会转换成对o.color求值。

2.每个函数都包含两个非继承而来的方法:apply()和call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设定函数体内

this对象的值。(这里重点说call())

function sum(num1 , num2){
    return num1 + num2;
}
function callSum( num1 , num2){
    return sum.call(this,num1,num2);
}
console.log(callSum(10,10))    //20

3.在call()方法下,必须明确传入每一个参数,结果与apply()没什么不同如何选择:如果打算传入arguments对象或者函数先接受到的是一个数组,则选用apply()更方便,否则选择call(),不传递参数的情况下无所谓。

4.传递参数并非apply()和call()的真正用武之地;他们真正强大的地方在于:能够扩充函数赖以运行的作用域:

window.color = 'red';
var o = { color : 'blue' };
function sayColor(){
    alert(this.color);
}
sayColor();    //red
sayColor.call(this);    //red
sayColor.call(window);    //red
sayColor.call(o);    //red 无耦合

这个例子在前面说明this对象的基础上修改,当运行sayColor.call(o)的时候,函数的执行环境发生了改变,因为this对象指向了o,所以是

blue。

5.使用call()(或者apply())来扩充作用域的最大好处:对象不需要与方法有任何耦合关系,上一个例子中先是将sayColor放到了对象中再执

行,而这次则不需要。

接下来

我们看柯里化后的尾递归代码:

function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);    //this指向全局,提供n,m参数。
  };
}

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

const factorial = currying(tailFactorial, 1);

factorial(5) // 120    //全局调用

看下factorial的输出:

ƒ (m) {
   return fn.call(this, m, n);
}

调用时将5传入作为tailFactorial的n,1作为taiFactarial的total值。

二、使用ES6默认值:

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

一旦使用递归,就最好使用尾递归。


严格模式

1.ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

2.这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

3.尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。


尾递归优化的实现 

1.采用“循环”换掉“递归”,蹦床函数(trampoline)可以将递归执行转为循环执行。

蹦床函数的实现:

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行,返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。

2.改写:

这里需要了解bind()函数:

bind(),ES5提出,这个方法会创建一个函数的实例其this会被绑定到传给bind()函数的值

window.color = 'red';
var o = {color:"blue"};
function sayColor(){
console.log(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor();    //blue

解析:

sayColor调用bind()传入对象,创建了objectSayColor函数,这个函数的this等于o,即使在全局调用也会看到Blue。

了解后我们来看改写的例子:

//跳板函数
function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();    //只要执行后返回一个函数就继续执行,最后将函数的输出赋值给变量f
  }
  return f;
}
//原函数
function sum(x,y){
  if( y > 0 ){
    return sum (x+1 , y-1);
  }else{
    return x;
  }
}
sum(1 , 100000) //超出栈

//改写sum函数使其每一步都返回一个函数
function sum2(x,y){
    if(y > 0){
        return sum2.bind(null,x,y);
    }else{
        return x;
    }
}
//使用跳板函数执行
trampoline(sum2(1,100000))    //正常输出10000001

尾递归优化的实现

正常模式下,或者那些不支持该功能的环境中的尾递归优化,将递归转变成循环:

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());  //apply(this,[1,10]) ->[2,9]->[3,8]... 
      }
      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

阮大神给出的原理:

默认情况下,active是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。

这里需要了解:

shift()方法:(会改变原数组,所以每次输出accmulated都是空数组)

shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。

如果数组是空的,那么 shift() 方法将不进行任何操作,返回 undefined 值。请注意,该方法不创建新数组,而是直接修改原有的 arrayObject。

解释几个点:

  1. 为什么出结果之前,sum返回的是undefined

因为,在y < 0之前,sum的返回值都是调用函数,并不存在明确的返回值,所以返回undefined

     2.f.apply(this,accmulated.shift())的作用?

将被删除的数组[1,10] / [2,8] .... 带入sum(x+1,y-1)中进行调用,一直到y = 0的时候停止返回x

过程解析:

我们先输出一下sum:

ƒ accumulator() {
            accumulated.push(arguments);    //  accumulated -> [[1,10]]
            log(arguments)
            if (!active) {
                active = true;
                while (acc…

开始详细解析:

//首先 调用了sum(1,10)
//相当于 
function accumulator() {
    accumulated.push(arguments);    //arguments变为[1,10] accumulated变为[[1,10]];
    if (!active) {
      active = true;
      while (accumulated.length) {    
        value = f.apply(this, accumulated.shift());    
        //shift()执行,返回数组被删除的第一个参数[1,10],value = f.apply(this,[1,10]),此时            
        //accumulated变为[]
      }
      active = false;
      return value;
    }
  };
}
//f.apply(this,[1,10]) 相当于调用sum(1+1,10-1) 即:sum(2,9),重复过程:
function accumulator() {
    accumulated.push(arguments);    //arguments变为[2,9] accumulated变为[[2,9]];
    if (!active) {
      active = true;
      while (accumulated.length) {    
        value = f.apply(this, accumulated.shift());    
        //shift()执行,返回数组被删除的第一个参数[2,9],value = f.apply(this,[2,9]),此时            
        //accumulated变为[]
      }
      active = false;
      return value;
    }
  };
}
//.................以此类推 , 当传入sum的y值变为0的时候,x为11,根据:
if (y > 0) {
  return sum(x + 1, y - 1)    
}else{
  return x    
}
// 得知y<0 return从undefined变成了明确数组x = 11
// 所以 sum(1,10)   = 11
    

斐波那契同理:

function tco(f) {
    var arr = [];
    var value;
    var active = false;
    return function () {
        arr.push(arguments);
        if (!active) {
            active = true;
            while (arr.length) {
                value = f.apply(this, arr.shift());
            }
            active = false;
            return value;
        }
    };
}
let Fibonacci = tco(function (n, x1 = 1, x2 = 1) {
    if (n <= 1) {
        return x2
    }
    return Fibonacci(n - 1, x2, x1 + x2);
})
log(Fibonacci(8))    //34

 


函数参数的尾逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章