JavaSrcipt 函数

目录

1. 函数内部

1.1 arguments

1.2 this

1.3 caller

1.4 new.target

2. 函数属性与函数方法

2.1 函数属性

2.2 函数方法

2.2.1 apply

2.2.2 call

2.2.3 bind

3. 尾调用优化

3.1 ES6内存管理优化前后对比

3.2 尾调用优化的条件

3.3 尾调用优化的代码

4. 闭包

4.1 this对象

4.2 内存泄漏

5. 私有变量

5.1 静态私有变量

5.2 模块模式

5.3 模块增强模式


1. 函数内部

在ECMAScript5中,函数内部存在两个特殊的对象:arguments和this。

ECMAScript6又增加了new.target属性。

1.1 arguments

arguments是一个类数组对象,包含调用函数是传入的所有参数。这个对象只有以function关键字定义函数时才会有(箭头函数没有)。arguments对象还有一个callee属性,指向arguments对象所在函数的指针。

阶乘函数:

  function factorial(num) {
    if(num <= 1) {
      return 1;
    } else {
      return num * factorial(num - 1);
    }
  }
  console.log(factorial(3)); // 6

像上面的例子函数名称和函数逻辑紧密耦合,如果函数名不一致会导致调用失败。可以使用arguments.callee解耦。

  function factorial(num) {
    if(num <= 1) {
      return 1;
    } else {
      return num * arguments.callee(num - 1);
    }
  }
  console.log(factorial(3)); // 6

重写之后的factorial()函数已经用arguments.callee代替了之前硬编码的factorial。这意味着无论函数叫什么名称,都可以引用正确的函数。

  let trueFactorial = factorial;
  factorial = function () {
    return 0;
  }
  console.log(trueFactorial(3)); // 6
  console.log(factorial(3)); // 0

这里,trueFactorial变量被赋值为factorial,实际上把同一个函数的指针又保存到了另一个位置。然后,factorial函数又被重写为一个返回0的函数。如果像factorial()最初的版本那样不使用arguments.callee,那么像上面这样调用trueFactorial()就会返回0。不过,通过将函数与名称解耦,trueFactorial()就可以正确计算阶乘,而factorial()则只能返回0。 

注:严格模式下不能使用callee

1.2 this

this在标准函数和箭头函数中游不同的行为。

在标准函数中,this指向随调用位置而改变,来看面的例子:

window.color = 'red';
let o = {
    color: 'blue'
};

function sayColor() {
    console.log(this.color);
}

sayColor(); // 全局调用,this指向window,输出red
o.sayColor = sayColor;
o.sayColor(); // 在对象中调用,this指向o,输出blue

在箭头函数中,this指向定义箭头函数的上下文,就是说箭头函数在哪里定义,this就指向哪里。

let o = {
    color: 'blue',
    o_sayColor() {
        console.log(this.color);
    }
};
let sayColor = () => {
    console.log(this.color);
}
sayColor(); // red
o.sayColor = sayColor;
o.sayColor(); // red
o.o_sayColor(); // blue

在事件回调或定时回调中调用某个函数时,this值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。因为箭头函数中的this会保留定义该函数时的上下文。

function a() {
    this.aname = 'a';
    // this引用a的实例
    setTimeout(() => {
        console.log(this.aname);
    })
}

function b() {
    this.bname = 'b';
    // this引用window对象
    setTimeout(function () {
        console.log(this.bname);
    })
}

new a(); // a
new b(); // undefined

1.3 caller

ES 5也会给函数对象上添加一个属性:caller。

这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null。比如:

function outer() {
    inner();
}

function inner() {
    console.log(inner.caller);
}

outer();
// ƒ outer() {
//     inner();
// }

以上代码会显示outer()函数的源代码。这是因为ourter()调用了inner(),inner.caller指向outer()。如果要降低耦合度,则可以通过arguments.callee.caller来引用同样的值:

function outer() {
    inner();
}

function inner() {
    console.log(arguments.callee.caller);
}

outer();
// ƒ outer() {
//     inner();
// }

1.4 new.target

检测函数是否使用new关键字实例化。

如果函数是正常调用的,则new.target的值是undefined;

如果是使用new关键字调用的,则new.target将引用被调用的构造函数。

function f() {
    if (!new.target) {
        console.log('必须使用new实例化f');
        throw '必须使用new实例化f'
    }
    console.log('使用new实例化f');
}

new f(); // 使用new实例化f
f(); // 必须使用new实例化f

function inner() {
    console.log(arguments.callee.caller);
}

2. 函数属性与函数方法

2.1 函数属性

每个函数都有两个属性:length和prototype。

length:函数参数的个数

prototype:保存引用类型所有实例方法,由所有实例共享。

function sayName(name) {
    console.log(name);
  }
  function sum(num1, num2) {
    return num1 + num2;
  }
  function sayHi() {
    console.log("hi");
  }
  console.log(sayName.length);   // 1
  console.log(sum.length);        // 2
  console.log(sayHi.length);     // 0

2.2 函数方法

函数还有三个方法:apply()、call()和bind()。

这三个方法都会以指定的this值来调用函数。

2.2.1 apply

apply()方法接收两个参数:函数内this的值和一个参数数组。

  function sum (a, b) {
    return a + b;
  }
  function applySum1 (a, b) {
    return sum.apply(this, arguments);
  }
  function applySum2 (a, b) {
    return sum.apply(this, [a, b]);
  }
  console.log(applySum1(10, 10)); // 20
  console.log(applySum2(15, 15)); // 30

2.2.2 call

call()方法与apply()的作用一样,只是传参的形式不同,必须将参数一个一个地列出来。

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

2.2.3 bind

bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。

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

3. 尾调用优化

ECMAScript 6规范新增了一项内存管理优化机制,让JavaScript引擎在满足条件时可以重用栈帧。

具体来说,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。

  function outerFunction() {
    return innerFunction(); // 尾调用
  }

3.1 ES6内存管理优化前后对比

在ES6优化之前,执行这个例子会在内存中发生如下操作。

(1)执行到outerFunction函数体,第一个栈帧被推到栈上。

(2)执行outerFunction函数体,到return语句。计算返回值必须先计算innerFunction。

(3)执行到innerFunction函数体,第二个栈帧被推到栈上。

(4)执行innerFunction函数体,计算其返回值。

(5)将返回值传回outerFunction,然后outerFunction再返回值。

(6)将栈帧弹出栈外。

在ES6优化之后,执行这个例子会在内存中发生如下操作。

(1)执行到outerFunction函数体,第一个栈帧被推到栈上。

(2)执行outerFunction函数体,到达return语句。为求值返回语句,必须先求值innerFunction。

(3)引擎发现把第一个栈帧弹出栈外也没问题,因为innerFunction的返回值也是outerFunction的返回值。

(4)弹出outerFunction的栈帧。

(5)执行到innerFunction函数体,栈帧被推到栈上。

(6)执行innerFunction函数体,计算其返回值。

(7)将innerFunction的栈帧弹出栈外。

ES6优化前每多调用一次嵌套函数,就会多增加一个栈帧。

ES6优化后无论调用多少次嵌套函数,都只有一个栈帧。

ES6尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁

3.2 尾调用优化的条件

尾调用优化的条件就是确定外部栈帧真的没有必要存在了。涉及的条件如下:

(1)代码在严格模式下执行;

(2)外部函数的返回值是对尾调用函数的调用;

(3)尾调用函数返回后不需要执行额外的逻辑;

(4)尾调用函数不是引用外部函数作用域中自由变量的闭包。

下面展示了几个违反上述条件的函数,因此都不符号尾调用优化的要求:

  "use strict";
  // 无优化:尾调用没有返回
  function outerFunction() {
    innerFunction();
  }
  // 无优化:尾调用没有直接返回
  function outerFunction() {
    let innerFunctionResult=innerFunction();
    returninnerFunctionResult;
  }
  // 无优化:尾调用返回后必须转型为字符串
  function outerFunction() {
    return innerFunction().toString();
  }
  // 无优化:尾调用是一个闭包
  function outerFunction() {
    let foo='bar';
    function innerFunction() { returnfoo;}
    return innerFunction();
  }

下面是几个符合尾调用优化条件的例子:

  "use strict";
  // 有优化:栈帧销毁前执行参数计算
  function outerFunction(a, b) {
    return innerFunction(a + b);
  }
  // 有优化:初始返回值不涉及栈帧
  function outerFunction(a, b) {
    if (a < b) {
      return a;
    }
    return innerFunction(a + b);
  }
  // 有优化:两个内部函数都在尾部
  function outerFunction(condition) {
    return condition ? innerFunctionA() : innerFunctionB();
  }

差异化尾调用和递归尾调用是容易让人混淆的地方。无论是递归尾调用还是非递归尾调用,都可以应用优化。引擎并不区分尾调用中调用的是函数自身还是其他函数。不过,这个优化在递归场景下的效果是最明显的,因为递归代码最容易在栈内存中迅速产生大量栈帧。

注意 之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用f.arguments和f.caller,而它们都会引用外部函数的栈帧。显然,这意味着不能应用优化了。因此尾调用优化要求必须在严格模式下有效,以防止引用这些属性

3.3 尾调用优化的代码

通过递归计算斐波纳契数列

  function fib(n) {
    if (n < 2) {
      return n;
    }
    return fib(n - 1) + fib(n - 2);
  }

  console.log(fib(0)); // 0
  console.log(fib(1)); // 1
  console.log(fib(2)); // 1
  console.log(fib(3)); // 2
  console.log(fib(4)); // 3
  console.log(fib(5)); // 5

显然这个函数不符合尾调用优化的条件,因为返回语句中有一个相加的操作。结果,fib(n)的栈帧数的内存复杂度是O(2n)。因此,即使这么一个简单的调用也可以给浏览器带来麻烦:

fib(1000);

将递归函数进行尾调用优化

首先,将其重构为满足优化条件的形式,使用两个嵌套的函数,外部函数作为基础框架,内部函数执行递归:

  function fib(n) {
    return fibImpl(0, 1, n);
  }

  function fibImpl(a, b, n) {
    if (n === 0) {
      return a;
    }
    return fibImpl(b, a + b, n - 1);
  }

这样重构之后,就可以满足尾调用优化的所有条件,再调用fib(1000)就不会对浏览器造成威胁了。

4. 闭包

匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常在嵌套函数中实现。

function createComparisonFunction(propertyName) {
  return function (object1, object2) {
    let value1 = object1[propertyName];
    let value2 = object2[propertyName];
    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  };
}

内部函数(匿名函数)引用了外部函数的变量propertyName。在这个内部函数被返回并在其他地方被使用后,它仍然引用着那个变量。

这是因为内部函数的作用域链包含createComparisonFunction()函数的作用域。

作用域链:在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。

然后用arguments和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。简单来说,由内到外访问作用域链。

在函数执行时,要从作用域链中查找变量,以便读、写值。来看下面的代码:

function compare(value1, value2) {
  if (value1 < value2) {
    return -1;
  } else if (value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}

let result = compare(5, 10);

这里定义的compare()函数是在全局上下文中调用的。第一次调用compare()时,会为它创建一个包含arguments、value1和value2的活动对象,这个对象是其作用域链上的第一个对象。而全局上下文的变量对象则是compare()作用域链上的第二个对象,其中包含this、result和compare。

在定义compare()函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。

4.1 this对象

在闭包中使用this会让代码变复杂。如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文。如果在全局函数中调用,则this在非严格模式下等于window,在严格模式下等于undefined。如果作为某个对象的方法调用,则this等于这个对象。匿名函数在这种情况下不会绑定到某个对象,这就意味着this会指向window,除非在严格模式下this是undefined。不过,由于闭包的写法所致,这个事实有时候没有那么容易看出来。来看下面的例子:

window.identity = 'The Window';
let object = {
  identity: 'My Object',
  getIdentityFunc() {
    return function() {
      return this.identity;
    };
  }
};
console.log(object.getIdentityFunc()()); // 'The Window'

此时返回的字符串是"The Winodw",即全局变量identity的值。为什么匿名函数没有使用其包含作用域(getIdentityFunc())的this对象呢?

前面介绍过,每个函数在被调用时都会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把this保存到闭包可以访问的另一个变量中,则是行得通的。比如:

  window.identity = 'The Window';
  let object = {
    identity: 'My Object',
    getIdentityFunc() {
      let that=this;
      return function() {
        returnthat.identity;
      };
    }
  };
  console.log(object.getIdentityFunc()()); // 'My Object'

 在定义匿名函数之前,先把外部函数的this保存到变量that中。然后在定义闭包时,就可以让它访问that。

4.2 内存泄漏

由于IE在IE9之前对JScript对象和COM对象使用了不同的垃圾回收机制(第4章讨论过),所以闭包在这些旧版本IE中可能会导致问题。在这些版本的IE中,把HTML元素保存在某个闭包的作用域中,就相当于宣布该元素不能被销毁。来看下面的例子:

  function assignHandler() {
    let element = document.getElementById('someElement');
    element.onclick = () => console.log(element.id);
  }

以上代码创建了一个闭包,即element元素的事件处理程序(事件处理程序将在第13章讨论)。而这个处理程序又创建了一个循环引用。匿名函数引用着assignHandler()的活动对象,阻止了对element的引用计数归零。只要这个匿名函数存在,element的引用计数就至少等于1。也就是说,内存不会被回收。

其实只要这个例子稍加修改,就可以避免这种情况,比如:

  function assignHandler() {
    let element = document.getElementById('someElement');
    let id = element.id;
    element.onclick = () => console.log(id);
    element = null;
  }

在这个修改后的版本中,闭包改为引用一个保存着element.id的变量id,从而消除了循环引用。不过,光有这一步还不足以解决内存问题。因为闭包还是会引用包含函数的活动对象,而其中包含element。即使闭包没有直接引用element,包含函数的活动对象上还是保存着对它的引用。因此,必须再把element设置为null。这样就解除了对这个COM对象的引用,其引用计数也会减少,从而确保其内存可以在适当的时候被回收。

5. 私有变量

任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量。私有变量包括函数参数、局部变量,以及函数内部定义的其他函数。来看下面的例子:

  function add(num1, num2) {
    let sum = num1 + num2;
    return sum;
  }

在这个函数中,函数add()有3个私有变量:num1、num2和sum。这几个变量只能在函数内部使用,不能在函数外部访问。如果这个函数中创建了一个闭包,则这个闭包能通过其作用域链访问其外部的这3个变量。基于这一点,就可以创建出能够访问私有变量的公有方法。

特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。

在对象上有两种方式创建特权方法。第一种是在构造函数中实现,比如:

  function MyObject() {
    // 私有变量和私有函数
    let privateVariable = 10;
    function privateFunction() {
      return false;
    }
    // 特权方法
    this.publicMethod = function() {
      privateVariable++;
      return privateFunction();
    };
  }

把所有私有变量和私有函数都定义在构造函数中。然后,再创建一个能够访问这些私有成员的特权方法。这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力。在这个例子中,变量privateVariable和函数privateFunction()只能通过publicMethod()方法来访问。在创建MyObject的实例后,没有办法直接访问privateVariable和privateFunction(),唯一的办法是使用publicMethod()。

如下面的例子所示,可以定义私有变量和特权方法,以隐藏不能被直接修改的数据:

  function Person(name) {
    this.getName = function() {
      return name;
    };
    this.setName = function (value) {
      name = value;
    };
  }
  let person = new Person('Nicholas');
  console.log(person.getName());   // 'Nicholas'
  person.setName('Greg');
  console.log(person.getName());   // 'Greg'

这段代码中的构造函数定义了两个特权方法:getName()和setName()。每个方法都可以构造函数外部调用,并通过它们来读写私有的name变量。在Person构造函数外部,没有别的办法访问name。因为两个方法都定义在构造函数内部,所以它们都是能够通过作用域链访问name的闭包。私有变量name对每个Person实例而言都是独一无二的,因为每次调用构造函数都会重新创建一套变量和方法。不过这样也有个问题:必须通过构造函数来实现这种隔离。构造函数模式的缺点是每个实例都会重新创建一遍新方法。使用静态私有变量实现特权方法可以避免这个问题。

5.1 静态私有变量

特权方法也可以通过使用私有作用域定义私有变量和函数来实现。这个模式如下所示:

  let MyObject;
  (function () {
    // 私有变量和私有函数
    let privateVariable = 10;

    function privateFunction() {
      return false;
    }

    // 构造函数
    MyObject = function () {
    };
    // 公有和特权方法
    MyObject.prototype.publicMethod = function () {
      privateVariable++;
      return privateFunction();
    };
  })();

在这个模式中,匿名函数表达式创建了一个包含构造函数及其方法的私有作用域。首先定义的是私有变量和私有函数,然后又定义了构造函数和公有方法。公有方法定义在构造函数的原型上,与典型的原型模式一样。注意,这个模式定义的构造函数没有使用函数声明,使用的是函数表达式。函数声明会创建内部函数,在这里并不是必需的。基于同样的原因(但操作相反),这里声明MyObject并没有使用任何关键字。因为不使用关键字声明的变量会创建在全局作用域中,所以MyObject变成了全局变量,可以在这个私有作用域外部被访问。注意在严格模式下给未声明的变量赋值会导致错误。

这个模式与前一个模式的主要区别就是,私有变量和私有函数是由实例共享的。因为特权方法定义在原型上,所以同样是由实例共享的。特权方法作为一个闭包,始终引用着包含它的作用域。来看下面的例子:

  let Person;
  (function() {
    let name = '';
    Person = function(value) {
      name = value;
    };
    Person.prototype.getName = function() {
      return name;
    };
    Person.prototype.setName = function(value) {
      name = value;
    };
  })();
  let person1 = new Person('Nicholas');
  console.log(person1.getName());   // 'Nicholas'
  person1.setName('Matt');
  console.log(person1.getName());   // 'Matt'
  let person2 = new Person('Michael');
  console.log(person1.getName());   // 'Michael'
  console.log(person2.getName());   // 'Michael'

这里的Person构造函数可以访问私有变量name,跟getName()和setName()方法一样。使用这种模式,name变成了静态变量,可供所有实例使用。这意味着在任何实例上调用setName()修改这个变量都会影响其他实例。调用setName()或创建新的Person实例都要把name变量设置为一个新值。而所有实例都会返回相同的值。

像这样创建静态私有变量可以利用原型更好地重用代码,只是每个实例没有了自己的私有变量。最终,到底是把私有变量放在实例中,还是作为静态私有变量,都需要根据自己的需求来确定。

5.2 模块模式

前面的模式通过自定义类型创建了私有变量和特权方法。而下面要讨论的Douglas Crockford所说的模块模式,则在一个单例对象上实现了相同的隔离和封装。单例对象(singleton)就是只有一个实例的对象。按照惯例,JavaScript是通过对象字面量来创建单例对象的,如下面的例子所示:

  let singleton = {
    name: value,
    method() {
      // 方法的代码
    }
  };

模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。模块模式的样板代码如下:

  let singleton = function() {
    // 私有变量和私有函数
    let privateVariable = 10;
    function privateFunction() {
      return false;
    }
    // 特权/公有方法和属性
    return {
      publicProperty: true,
      publicMethod() {
        privateVariable++;
        return privateFunction();
      }
    };
  }();

5.3 模块增强模式

另一个利用模块模式的做法是在返回对象之前先对其进行增强。这适合单例对象需要是某个特定类型的实例,但又必须给它添加额外属性或方法的场景。来看下面的例子:

  let singleton = function() {
    // 私有变量和私有函数
    let privateVariable = 10;
    function privateFunction() {
      return false;
    }
    // 创建对象
    let object = new CustomType();
    // 添加特权/公有属性和方法
    object.publicProperty = true;
    object.publicMethod = function() {
      privateVariable++;
      return privateFunction();
    };
    // 返回对象
    return object;
  }();

  • 26
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CJ-杰

打赏描述

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值