在ECMAScript 5中,函数内部存在两个特殊的对象:arguments和this
    ECMAScript 6又新增了new.target属性
    10.9.1 arguments
    arguments对象是一个类数组对象,包含调用函数时传入的所有参数。
    这个对象只有以function关键字定义函数(相对于使用箭头语法创建函数)时才会有。
    arguments对象有一个callee属性,指向arguments对象所在函数的指针。
    来看下面这个经典的阶乘函数:

    1. function factorial(num) {
    2. if (num <= 1) {
    3. return 1;
    4. } else {
    5. return num * factorial(num - 1);
    6. }
    7. }

    阶乘函数一般定义成递归调用的,就像上面这个例子。
    只要给函数一个名称,而且这个名称不会变,这样定义就没有问题。
    但是,这个函数要正确执行就必须保证函数名是factorial,从而导致了紧密耦合。
    使用arguments.callee,可以让函数逻辑与函数名解耦:

    1. function factorial(num) {
    2. if (num <= 1) {
    3. return 1;
    4. } else {
    5. return num * arguments.callee(num - 1);
    6. }
    7. }

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

    1. function factorial(num) {
    2. if (num <= 1) {
    3. return 1;
    4. } else {
    5. return num * arguments.callee(num - 1);
    6. }
    7. }
    8. let trueFactorial = factorial;
    9. factorial = function() {
    10. return 0;
    11. };
    12. console.log(trueFactorial(5)); // 120
    13. console.log(factorial(5)); // 0

    trueFactorial变量被赋值为factorial,实际上把同一个函数的指针又保存到了另一个位置。
    然后,factorial函数又被重写为一个返回0的函数。
    如果像factorial()最初的版本那样不使用arguments.callee,那么像上面这样调用trueFactorial()就会返回0。
    不过,通过将函数与名称解耦,trueFactorial()就可以正确计算阶乘,而factorial()则只能返回0。
    10.9.2 this
    另一个特殊的对象是this,它在标准函数和箭头函数中有不同的行为。
    在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时候通常称其为this值(在网页的全局上下文中调用函数时,this指向windows)。
    来看下面的例子:

    1. window.color = 'red';
    2. let o = {
    3. color: 'blue'
    4. };
    5. function sayColor() {
    6. console.log(this.color);
    7. }
    8. sayColor(); // red
    9. o.sayColor = sayColor;
    10. o.sayColor(); // blue

    定义在全局上下文中的函数sayColor()引用了this对象。
    这个this到底引用哪个对象必须到函数被调用时才能确定。因此这个值在代码执行的过程中可能会变。
    如果在全局上下文中调用sayColor(),这结果会输出”red”,因为this指向window,而this.color相当于window.color。
    而在把sayColor()赋值给o之后再调用o.sayColor(), this会指向o,即this.color相当于o.color,所以会显示”blue”。
    在箭头函数中,this引用的是定义箭头函数的上下文。
    下面的例子演示了这一点。
    在对sayColor()的两次调用中,this引用的都是window对象,因为这个箭头函数是在window上下文中定义的:

    1. window.color = 'red';
    2. let o = {
    3. color: 'blue'
    4. };
    5. let sayColor = () => console.log(this.color);
    6. sayColor(); // red
    7. o.sayColor = sayColor;
    8. o.sayColor(); // red

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

    1. function King() {
    2. this.royaltyName = 'Henry';
    3. // this引用King的实例
    4. setTimeout(() => console.log(this.royaltyName), 1000);
    5. }
    6. function Queen() {
    7. this.royaltyName = 'Elizabeth';
    8. // this引用window对象
    9. setTimeout(function() { console.log(this.royaltyName) }, 1000);
    10. }
    11. new King(); // Henry
    12. new Queen(); // undefined

    注:函数名只是保存指针的变量。
    因此全局定义的sayColor()函数和o.sayColor()是同一个函数,只不过执行的上下文不同。
    10.9.3 caller
    ECMAScript 5给函数对象上添加一个属性:caller。
    这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null。
    比如:

    1. function outer() {
    2. inner();
    3. }
    4. function inner() {
    5. console.log(inner.caller);
    6. }
    7. outer();
    8. // ƒ outer() {
    9. // inner();
    10. // }
    11. // 显示outer()函数的源代码。这是因为ourter()调用了inner(), inner.caller指向outer()

    如果要降低耦合度,则可以通过arguments.callee.caller来引用同样的值:

    1. function outer() {
    2. inner();
    3. }
    4. function inner() {
    5. console.log(arguments.callee.caller);
    6. }
    7. outer();
    8. // ƒ outer() {
    9. // inner();
    10. // }

    在严格模式下访问arguments.callee会报错。
    ECMAScript 5也定义了arguments.caller,但在严格模式下访问它会报错,在非严格模式下则始终是undefined。这是为了分清arguments.caller和函数的caller而故意为之的。
    而作为对这门语言的安全防护,这些改动也让第三方代码无法检测同一上下文中运行的其他代码。
    严格模式下还有一个限制:不能给函数的caller属性赋值,否则会导致错误。
    10.9.4 new.target
    ECMAScript中的函数,始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。
    ECMAScript 6新增了,检测函数是否使用new关键字调用的new.target属性。
    如果函数是正常调用的,则new.target的值是undefined;
    如果是使用new关键字调用的,则new.target将引用被调用的构造函数。

    1. function King() {
    2. if (!new.target) {
    3. throw 'King must be instantiated using "new"'
    4. }
    5. console.log('King instantiated using "new"')
    6. }
    7. new King(); // King instantiated using "new"
    8. King(); // 报错:Uncaught King must be instantiated using "new"

    10.10 函数属性与方法
    ECMAScript中的函数是对象,因此有属性和方法。
    每个函数都有两个属性:length和prototype
    其中,length属性保存函数定义的命名参数的个数
    如下例所示:

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

    以上代码定义了3个函数,每个函数的命名参数个数都不一样。
    sayName()函数有1个命名参数,所以其length属性为1。
    sum()函数有两个命名参数,所以其length属性是2。
    sayHi()没有命名参数,其length属性为0。
    prototype是保存引用类型所有实例方法的地方。
    这意味着toString()、valueOf()等方法实际上都保存在prototype上,进而由所有实例共享。
    这个属性在自定义类型时特别重要。
    在ECMAScript 5中,prototype属性是不可枚举的,因此使用for-in循环不会返回这个属性。
    函数还有两个方法:apply()和call()。
    这两个方法都会以指定的this值来调用函数,即会设置调用函数时函数体内this对象的值。
    apply()方法接收两个参数:函数内this的值和一个参数数组。第二个参数可以是Array的实例,但也可以是arguments对象。
    来看下面的例子:

    1. function sum(num1, num2) {
    2. return num1 + num2;
    3. }
    4. function callSum1(num1, num2) {
    5. return sum.apply(this, arguments); // 传入arguments对象
    6. }
    7. function callSum2(num1, num2) {
    8. return sum.apply(this, [num1, num2]); // 传入数组
    9. }
    10. console.log(callSum1(10, 10)); // 20
    11. console.log(callSum2(10, 10)); // 20

    在这个例子中,callSum1()会调用sum()函数,将this作为函数体内的this值(这里等于window,因为是在全局作用域中调用的)传入,同时还传入了arguments对象。callSum2()也会调用sum()函数,但会传入参数的数组。这两个函数都会执行并返回正确的结果。
    注:在严格模式下,调用函数时如果没有指定上下文对象,则this值不会指向window。除非使用apply()或call()把函数指定给一个对象,否则this的值会变成undefined。
    call()方法与apply()的作用一样,只是传参的形式不同。
    第一个参数跟apply()一样,也是this值,而剩下的要传给被调用函数的参数则是逐个传递的。
    换句话说,通过call()向函数传参时,必须将参数一个一个地列出来
    比如:

    1. function sum(num1, num2) {
    2. return num1 + num2;
    3. }
    4. function callSum(num1, num2) {
    5. return sum.call(this, num1, num2);
    6. }
    7. console.log(callSum(10, 10)); // 20

    这里的callSum()函数必须逐个地把参数传给call()方法。结果跟apply()的例子一样。
    到底是使用apply()还是call(),取决于怎么给要调用的函数传参更方便
    如果想直接传arguments对象或者一个数组,那就用apply();
    否则,就用call()
    当然,如果不用给被调用的函数传参,则使用哪个方法都一样。
    apply()和call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内this值的能力。
    考虑下面的例子:

    1. window.color = 'red';
    2. let o = {
    3. color: 'blue'
    4. };
    5. function sayColor() {
    6. console.log(this.color);
    7. }
    8. sayColor(); // red
    9. sayColor.call(this); // red
    10. sayColor.call(window); // red
    11. sayColor.call(o); // blue

    这个例子是在之前那个关于this对象的例子基础上修改而成的。同样,sayColor()是一个全局函数,如果在全局作用域中调用它,那么会显示”red”。这是因为this.color会求值为window.color。如果在全局作用域中显式调用sayColor.call(this)或者sayColor.call(window),则同样都会显示”red”。而在使用sayColor.call(o)把函数的执行上下文即this切换为对象o之后,结果就变成了显示”blue”了。使用call()或apply()的好处是可以将任意对象设置为任意函数的作用域,这样对象可以不用关心方法。在前面例子最初的版本中,为切换上下文需要先把sayColor()直接赋值为o的属性,然后再调用。而在这个修改后的版本中,就不需要这一步操作了。ECMAScript 5出于同样的目的定义了一个新方法:bind()。bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。比如:

    1. window.color = 'red';
    2. let o = {
    3. color: 'blue'
    4. };
    5. function sayColor() {
    6. console.log(this.color);
    7. }
    8. let objectSayColor = sayColor.bind(o);
    9. objectSayColor(); // blue

    在sayColor()上调用bind()并传入对象o创建了一个新函数objectSayColor()
    objectSayColor()中的this值被设置为o,因此直接调用这个函数,即使是在全局作用域中调用,也会返回字符串”blue”
    对函数而言,继承的方法toLocaleString()和toString()始终返回函数的代码。返回代码的具体格式因浏览器而异。有的返回源代码,包含注释,而有的只返回代码的内部形式,会删除注释,甚至代码可能被解释器修改过。
    由于这些差异,因此不能在重要功能中依赖这些方法返回的值,而只应在调试中使用它们。继承的方法valueOf()返回函数本身。
    10.11 函数表达式
    定义函数有两种方式:函数声明和函数表达式。
    函数声明是这样的:

    1. function functionName(arg0, arg1, agr2) {
    2. // 函数体
    3. }

    函数声明的关键特点是函数声明提升,即函数声明会在代码执行之前获得定义。这意味着函数声明可以出现在调用它的代码之后。因为JavaScript引擎会先读取函数声明,然后再执行代码
    第二种创建函数的方式就是函数表达式。
    函数表达式有几种不同的形式,最常见的是这样的:

    1. let functionName = function(arg0, arg1, agr2) {
    2. // 函数体
    3. }

    函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量functionName。这样创建的函数叫作匿名函数(anonymous funtion),因为function关键字后面没有标识符。(匿名函数有也时候也被称为兰姆达函数)。未赋值给其他变量的匿名函数的name属性是空字符串。
    函数表达式跟JavaScript中的其他表达式一样,需要先赋值再使用。

    1. sayHi();
    2. function sayHi() {
    3. console.log('Hi!');
    4. }
    5. // Hi!
    1. sayHi();
    2. let sayHi = function() {
    3. console.log('Hi!');
    4. }
    5. // Uncaught ReferenceError: Cannot access 'sayHi' before initialization

    理解函数声明与函数表达式之间的区别,关键是理解提升。
    比如:

    1. // 千万不要这样做!
    2. if (condition) {
    3. function sayHi() {
    4. console.log('Hi!');
    5. }
    6. } else {
    7. function sayHi() {
    8. console.log('Hi!');
    9. }
    10. }

    这段代码看起来很正常,就是如果condition为true,则使用第一个sayHi()定义;否则,就使用第二个。
    事实上,这种写法在ECAMScript中不是有效的语法。JavaScript引擎会尝试将其纠正为适当的声明。
    问题在于浏览器纠正这个问题的方式并不一致。多数浏览器会忽略condition直接返回第二个声明。Firefox会在condition为true时返回第一个声明。这种写法很危险,不要使用。
    不过,如果把上面的函数声明换成函数表达式就没问题了:

    1. // 没问题
    2. let sayHi;
    3. if (condition) {
    4. function sayHi() {
    5. console.log('Hi!');
    6. }
    7. } else {
    8. sayHi = function() {
    9. console.log('Hi!');
    10. }
    11. }

    10.12 递归
    递归函数通常的形式是一个函数通过名称调用自己
    如下面的例子所示:

    1. function factorial(num) {
    2. if (num <= 1) {
    3. return 1;
    4. } else {
    5. return num * factorial(num - 1);
    6. }
    7. }

    这是经典的递归阶乘函数。虽然这样写是可以的,但如果把这个函数赋值给其他变量,就会出问题:

    1. let anotherFactorial = factorial;
    2. factorial = null;
    3. console.log(anotherFactorial(4));
    4. // Uncaught TypeError: factorial is not a function at factorial

    这里把factorial()函数保存在了另一个变量anotherFactorial中,然后将factorial设置为null,于是只保留了一个对原始函数的引用。
    而在调用anotherFactorial()时,要递归调用factorial(),但因为它已经不是函数了,所以会出错。
    在写递归函数时使用arguments.callee可以避免这个问题。
    arguments.callee就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用,
    如下所示:

    1. function factorial(num) {
    2. if (num <= 1) {
    3. return 1;
    4. } else {
    5. return num * arguments.callee(num - 1);
    6. }
    7. }

    把函数名称替换成arguments.callee,可以确保无论通过什么变量调用这个函数都不会出问题。
    因此在编写递归函数时,arguments.callee是引用当前函数的首选
    不过,在严格模式下运行的代码不能访问arguments.callee,因为访问会出错
    此时,可以使用命名函数表达式(named function expression)达到目的
    比如:

    1. const factorial = (function f(num) {
    2. if (num <= 1) {
    3. return 1;
    4. } else {
    5. return num * f(num - 1);
    6. }
    7. })

    创建了一个命名函数表达式f(),然后将它赋值给了变量factorial。
    即使把函数赋值给另一个变量,函数表达式的名称f也不变,因此递归调用不会有问题。
    这个模式在严格模式和非严格模式下都可以使用。