《JavaScript高级程序设计》第十章:函数,读书笔记

57 阅读22分钟

本文介绍《JavaScript高级程序设计》的10章-函数,看完你将能回答如下问题:

  1. 声明一个函数有几种方式?【10.0】
  2. 函数声明和函数表达式有区别吗?【10.7】
  3. 箭头函数和普通函数有什么区别?【10.1 2】
  4. 函数重载【10.4】
  5. 了解arguments.callee对象吗?了解arguments.callee.caller吗?【10.9】
  6. 了解new.target属性吗?【10.9】
  7. 函数的方法 call apply bind区别?【10.10】
  8. 了解函数的尾调用吗?【10.13】
  9. 了解函数的闭包吗?闭包的优缺点是什么?【10.14】

10 函数

10.0 声明函数的方式

// 声明函数的四种方式
// function 关键字声明
function sum (num1, num2) {
    return num1 + num2
}
console.log(sum(1, 2)); // 3

// 函数表达式
let sum2 = function (num1, num2) {
    return num1 + num2
};

// 箭头函数
let sum3 = (num1, num2) => {
    return num1 + num2
}

// 构造函数
let sum4 = new Function('num1', 'num2', 'return num1 + num2')
console.log(sum4(1, 2), 'sum(1, 2)'); // 3

10.1 箭头函数

1 箭头函数语法

  1. 函数声明:
let fn1 = (a, b) => {
    return a + b
}
// 等同于
let fn2 = function (a, b) {
    return a + b
}
  1. 参数是否需要圆括号:
// 不需要参数或需多个参数,使用(),
let fn3 = () => {
    return 1
}
let fn4 = (a, b) => {
    return a + b
}
// 一个参数可以省略圆括号
let fn5 = x => {
    return x * 3
}
  1. 如果代码里面超过一行代码,需要使用大括号:
let fn6 = (a) => {
    let b = a + 3
    return b
}
  1. 当只有一行代码时,不需要大括号,且会默认返回箭头后面的值:
let fn7 = (a) => a + 3

5. 返回对象的错误写法:

let fn8 = (a) => {a: 1}
console.log(fn8(), 'fn8()'); // undefined
  1. 返回对象应该增加圆括号在外面:
let fn9 = () => ({a: 1})
console.log(fn9(), 'fn9()'); // {a: 1}  'fn9()'
  1. 不需要返回值时这样写,用void关键字:
// 不需要任何返回的情形
let fn10 = () => void 222
console.log(fn10(), 'fn10()'); // undefined
  1. 可以和解构赋值配合起来使用,从对象中解构出num1和num2的属性值
const fn11 = ({num1, num2}) => {
    return num1 + num2
}
console.log(fn11({num1: 1, num2: 2}), 'fn(11)'); // 3
  1. 和rest参数配合起来使用, 如下a形参对应第一个参数,后续的所有参数都给到arr,是一个数组
const fn12 = (a, ...arr) => {
    console.log(a, 'a'); // 1
    console.log(arr, 'arr'); // [2, 3, 4, 5]
}
console.log(fn12(1, 2, 3, 4, 5));
  1. 简化函数写法
    可以看出普通函数写法不直观,代码还要多一些
// 普通函数的写法
[1, 2, 3].map(item => {
    return x * x
});
// 箭头函数的写法
[1, 2, 3].map(() => x * x)

2 箭头函数注意事项

  1. 箭头函数不能使用new命令,否则会直接报错
// 普通函数使用new命令
function Fn1 (a, b) {
    this.a = a
    this.b = b
}
let fn1 = new Fn1(1, 2)
console.log(fn1, 'fn1');

// 箭头函数不能用new命令
let Fn2 = (a, b) => {
    this.a = a
    this.b = b
}
let fn2 = new Fn2(3, 4)
console.log(fn2, 'fn2'); // Uncaught TypeError: Fn2 is not a constructor
  1. 不可以使用arguments对象,否则会直接提示arguments对象是不存在的
// argumetns对象
let fn3 = function (a, b) {
    console.log(arguments);
}
fn3(3, 4)
// 不存在arguments对象
let fn4 = (a, b) => {
    console.log(arguments, 'arguments'); // Uncaught ReferenceError: arguments is not defined
}
fn4(4, 5)
  1. 不可以使用yield命令
  2. 箭头函数没有自己的this指向

示例一:

function fn() {
    setTimeout(() => {
        console.log('id:', this.id); // 求这里的this打印的是什么
    }, 100);
}
let id = 21;

fn.call({ id: 42 });

如上代码中,打印的id是42,因为setTimeout里用的箭头函数,该箭头函数在fn函数生效时生成,真正执行在100ms以后,其this指向就是上下文里面的this,就是fn执行时被call绑定的this。如果是普通函数,setTimeout里面的this就是window全局对象

在这里打印this:

function fn() {
+    console.log(this, 'this11');
    setTimeout(() => {
+        console.log(this, 'this13');
        console.log('id:', this.id); // 42
    }, 100);
}
let id = 21;

fn.call({ id: 42 });

发现this就是指向对象:

示例二:

function Timer() {
    this.s1 = 0;
    this.s2 = 0;
    // 箭头函数
    setInterval(() => this.s1++, 1000);
    // 普通函数
    setInterval(function () {
        this.s2++;
    }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100); // 分析打印的是什么
setTimeout(() => console.log('s2: ', timer.s2), 3100); // 分析打印的是什么

上面代码中,Timer构造函数中,执行了箭头函数和普通的函数,箭头函数的this绑定的是实例对象timer,普通函数的this绑定的是window全局对象,所以timer.s1更新了3次因为setInterval执行了3次,timer.s2一次都没有更新

箭头函数用来固定this:

var handler = {
  id: '123456',

  init: function() {
    document.addEventListener('click',
      event => this.doSomething(event.type), false);
  },

  doSomething: function(type) {
    console.log('Handling ' + type  + ' for ' + this.id);
  }
};

handler对象中的init方法里面,绑定了click事件监听,因为箭头函数,this.doSomething始终指向handler对象,如果是普通函数,这里的this会指向全局对象,将访问不到doSomething方法

示例三:

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

foo.call绑定到{id: 1}上,所以后续无论怎么执行,this都绑定到{id: 1}身上

示例四:

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });

如上this指向立即执行函数的上下文,被call绑定到{x: 'outer'}上,而不是{x: 'inner'}上

3 箭头函数不适合的场景

如下this会指向全局,而不是对象,无法访问到值33

let handler = {
    s: 33,
    fn: () => {
        return ++this.s
    }
}
console.log(handler.fn(), ' handler.fn()'); // NaN

如下场景也不适合,里面的this会指向window,实际想要他指向按钮本身,用普通函数就没事了

let btn = document.getElementById('btn');
btn.addEventListener('click', () => {
    this.classList.toggle('on');
});

4 箭头函数没有自己的this关键字,this指向取决于 箭头函数定义时 所在的作用域 的this指向

4.1 普通函数 this指向(谁调用,指向谁)
 var name = 'window'; // 其实是window.name = 'window'

        var A = {
            name: 'A',
            sayHello: function () {
                console.log(this.name)
            }
        }

        A.sayHello();// 输出A

        var B = {
            name: 'B'
        }

        A.sayHello.call(B);//输出B

        // 切记最后这里指向的是window对象
        A.sayHello.call();//不传参数指向全局window对象,输出window.name也就是window
4.2 箭头函数的this指向 window时
 var name = 'window';

        var A = {
            name: 'A',
            sayHello: () => {
                console.log(this.name)
            }
        }

        A.sayHello();// 还是以为输出A ? 错啦,其实输出的是window
        // 这里的箭头函数没有绑定在A身上,
        // 箭头函数的this指向根据 该函数所在的作用域 指向的对象
        // 这里的箭头函数外层没有别的函数包裹,而对象没有作用域,所以所在的作用域在最外部js环境就是window对象
4.3 箭头函数this指向 (指向对象时)

注意:外层有函数,函数是箭头函数的作用域;函数被对象调用;箭头函数作用域也指向镀锡

 var name = 'window';

var A = {
    name: 'A',
    sayHello: function () {
        var s = () => console.log(this.name)
        return s//返回箭头函数s
    }
}
// console.log(A.sayHello()); // 输出的是箭头函数
var sayHello = A.sayHello();
sayHello();// 输出A

var B = {
    name: 'B'
}

sayHello.call(B); //还是A
sayHello.call(); //还是A
// 这里的箭头函数s所在的作用域是 sayHello  因为sayHello是一个函数 外层有函数
// A.sayHello() 函数被A调用 函数的this指向A
// s箭头函数跟随sayHello这个作用域 this指向A

分析2: 结合bind

var fn = function () {
    return () => {
        console.log(this); // 2.1 指向的是window 很出乎意料 以为是obj
        // 2.2 箭头函数没有绑定this this指向谁 取决于箭头函数所在的作用域指向			的对象 一定是所在的作用域指向的对象
        // 2.3 箭头函数在大的函数fn里面,作用域是fn函数
        // 2.4 fn()()是 可以理解为 window.fn()()
        // 2.5 所以fn函数可以理解为是window调用的,
        // 2.6 所以箭头函数受作用域的影响也指向window

    }
}.bind(obj);
var obj = { name: 'lisi' };
// 1.1 fn()调用上面的函数得到一个返回值,是一个匿名函数 箭头函数的形式
// 1.2 同时借助了bind方法,想让外面的大函数的this指向obj
// 1.3 再次加括号,调用匿名函数
// 1.4 问输出是什么?你以为是obj嘛?错啦
fn()();
4.4 箭头函数不传递参数
var f = v => v;
console.log(f()); // 输出是undefined 因为f()并没有传递参数进去
4.5 箭头函数结合 内置对象
function push(array, items) {
    items.forEach((item) => {
        array.push(item);
        console.log(this); // 三次指向的都是window
    })
    // apply方法,借助数学内置对象 求数组最大值
    // this改变 指向为 null
    return Math.max.apply(null, items);
}
let arr = [];
let res = push(arr, [1, 2, 3]); // 3
console.log(res);

10.2 函数名

// ES的函数名就是指向函数的指针
function sum1 (num1, num2) {
  return num1, num2
}
let sum2 = sum1 // sum2也指向同一个函数
console.log(sum1(1, 2)); // 2
console.log(sum2(1, 2)); // 2

函数具有 name 属性

console.log(sum1.name); // 'sum1'
console.log(sum3.name); // 'sum3'
console.log((() => {}).name); // 空字符串
console.log(sum4.name); // 'anonymous'

10.3 理解参数

  1. 假设函数定义了 2 个形参,调用时传 1 个还是传 3 个都没关系,因为参数在函数内部表现的就是数组,argumetns 伪数组
// ES的函数名就是指向函数的指针
function fn (num1, num2) {
    console.log(arguments, 'arguments');
    return num1 + num2
} 
console.log(fn(1, 2));

可以直接用 arguments 伪数组,

// ES的函数名就是指向函数的指针
function fn2 (num1, num2) {
    return arguments[0] + arguments[1]
} 
console.log(fn(1, 2)); // 3
console.log(fn2(1, 2)); // 3

也可以 arguments 和形参联合起来使用

// ES的函数名就是指向函数的指针
function fn2 (num1, num2) {
    return arguments[0] + num2
} 
console.log(fn(1, 2)); // 3
console.log(fn2(1, 2)); // 3

修改 arguments 的值能够影响形参,

function fn3 (num1, num2) {
  arguments[1] = 10
  return arguments[0] + num2
} 
console.log(fn3(1, 2)); // 11

反过来修改是不可以的,改 num2 形参 => 影响 arguments 的值,这里我还是有点疑问,说是说不可,但是为什么打印出来还是 4 呢

// ES的函数名就是指向函数的指针
function fn4 (num1, num2) {
    num2 = 3
    return arguments[0] + arguments[1]
} 
console.log(fn4(1, 2)); // 4

只传递一个参数,改了 arguments[1] 的值,不会影响 num2

// ES的函数名就是指向函数的指针
function fn5 (num1, num2) {
    arguments[1] = 3
    console.log(num2, 'num2');
} 
console.log(fn5(1) ,'fn5'); // undefined

严格模式下:

  1. arguments[1] 被修改后,num2 还是之前的值
  2. 重写 arguments 对象会报错
// ES的函数名就是指向函数的指针
function fn3 (num1, num2) {
    "use strict"; // 当前函数开启严格模式
    arguments[1] = 10
    return arguments[0] + num2
} 
console.log(fn3(1, 2)); // 3

注意:

  1. 箭头函数无法访问到 arguments 对象
  2. 可以通过包装函数把参数给到箭头函数
let fn = function () {
  // 来自这里 arguments
  let bar = () => {
    console.log(arguments, 'arguments')
  }
}

10.4 没有重载

重载是:在作用域内可以定义多个重名的函数,调用函数时,解释器和编译器,根据传递参数的类型和数量,决定执行哪个函数

JS 没有重载:因为 JS 的变量类型和数量是在函数执行时才确定的

JS 可以根据 arguments 的数量来模拟重载

function example(param1, param2) {
    if (arguments.length === 1) {
        // 处理一个参数的情况
    } else if (arguments.length === 2) {
        // 处理两个参数的情况
    }
}

10.5 默认参数值

形参给默认参数值:

function fn (name = '123') {
  console.log(name, 'name');
}
console.log(fn('xhg'));

形参和实参不匹配的情况:

  1. 形参和实参正好匹配,正常执行
  2. 实参多于形参,只取到形参的个数去执行
  3. 如果实参的个数小于形参的个数,多出来的形参会被定义未undefined,然后最终的结果就是NaN。
  4. 一个参数声明了没有赋值,输出它,就是undefined

给参数传递 undefined,则 name 值用默认的,age 用的是传入的

// 给参数传递undefined
function fn2 (name = '123', age = 22) {
    console.log(name, 'name'); // '123'
    console.log(age, 'age'); // 21
}
console.log(fn2(undefined, 21));

arguments始终以传入的值为准,不反应形式参数

function fn3 (name = '123') {
  console.log(arguments, 'fn3 arguments'); // arguments[0]是''
}
console.log(fn3(''));

形参的默认参数 可以使用函数调用的结果,注意,fn4 没有传入参数,所以才会去调用 getNum

// 形参可以使用函数调用的结果
let values = ['I', 'II']
let ordinary = 0;
function getNum () {
    return values[ordinary++]
}
function fn4 (name = getNum()) {
    return `King ${name}`
}
console.log(fn4(), 'fn4()'); // 'I'
console.log(fn4(), 'fn4()'); // 'II'

箭头函数可以使用默认参数,必须加括号

const fn5 = (name = 'xhg') => name
console.log(fn5(), 'fn5'); // xhg

10.5.1 默认参数作用域和暂时性死区

把name和age这里当做是let声明,先声明 name,再声明 age

function fn2 (name = '123', age = 21) {
    console.log(name, 'name'); // '123'
    console.log(age, 'age'); // 21
}
console.log(fn2());

age 可以用 name 的值

function fn2 (name = '123', age = name) {
    console.log(name, 'name'); // '123'
    console.log(age, 'age'); // 21
}
console.log(fn2());

name 不能用 age 的值

// 错误
function fn3 (name = age, age = 21) {
    console.log(name, 'name'); // '123'
    console.log(age, 'age'); // 21
}
console.log(fn2());

不能用 函数 里面作用域的值

// 错误,
function fn4 (name = '123', age = a) {
    let a = 123
}
console.log(fn4());

10.6.1 参数扩展和收集

像这样的方式进行扩展:getSum(...value)

// 扩展参数
function getSum() {
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i]
    }
    return sum;
}
// 假设有一个数组[1,2,3,4] 肯定不能直接这样
const value = [1,2,3,4]
console.log(getSum(value)); // 0 + [1,2,3,4] = '01,2,3,4'
console.log(getSum(...value)); // 10 这样是对的

扩展运算符前面和后面传其他值都没有关系

console.log(getSum(...[3,3,3], ...value, 3, 4), 'getSum(...[3,3,3], ...value, 3, 4)'); 
// 26

arguments 不关心是否用了扩展运算符

function getArgumentsLength () {
    console.log(arguments.length);
}
console.log(getArgumentsLength(-1, ...value)); // 5
console.log(getArgumentsLength(...value, 5)); // 5
console.log(getArgumentsLength(-1, ...value, 5)); // 6
console.log(getArgumentsLength(...value, ...[5,6,7])); // 7

扩展运算符不影响默认参数

// 扩展运算符不影响默认参数
function getProduct (a, b, c = 1) {
    return a * b * c
}
console.log(getProduct(1, 2)); // 2

10.6.2 收集参数

把传入的参数收集为数组

function getValue (...values) {
    console.log(x, 'x'); // 每一轮累进的值
    console.log(y, 'y'); // 每一轮的数据项
    return values.reduce((x, y) => x += y, 0)
}
console.log(getValue(1, 2 ,3 ,4));

可以

function getValue2 (num1, ...values) {
    console.log(num1, 'num1'); // 1
    console.log(values, 'values'); // [2, 3, 4]
}
console.log(getValue2(1, 2 ,3 ,4));

不可以, 收集参数不能放前面

function getValue2 (...values, num1) {
  console.log(num1, 'num1'); // 1
  console.log(values, 'values'); // [2, 3, 4]
}
console.log(getValue2(1, 2 ,3 ,4));

收集参数不影响 arguments 的个数

function getValue3 (...value) {
    console.log(arguments.length, 'arguments'); // 3
}
console.log(getValue3(1,2,3), 'getValue3');

10.7 函数声明和函数表达式

函数声明,如下 ok,JS 引擎会把函数声明直接提到所有代码最顶部

// 函数声明 会提前声明
console.log(sum(1, 2)); // 3
function sum (num1, num2) {
    return num1 + num2
}

函数表达式不 ok,函数表达式一定是执行到这一行代码,才声明

console.log(sum2(1, 2)); // 3 会报错
let sum2 = function (num1, num2) {
  return num1 + num2
}

改成 var 关键字也不行,原因就是这个函数定义在一个变量初始化的语句中

console.log(sum2(1, 2)); // 3 会报错
var sum2 = function (num1, num2) {
    return num1 + num2
}

10.8 函数作为值

把函数作为参数传递

function doSomething (fn, someArgument) {
    return fn(someArgument)
}

function add (num) {
    return num + 10
}

function hello (name) {
    return `hello ${name}`
}

console.log(doSomething(add, 10)); // 20
console.log(doSomething(hello, 'xhg')); // 'hello xhg'

把函数作为返回值返回,如下写一个根据 name 和 age 来进行排序的函数

// 把函数作为返回值返回
function createCompareFunction (propertyName) {
    return function (obj1, obj2) {
        let value1 = obj1[propertyName]
        let value2 = obj2[propertyName]

        if (value1 < value2) {
            return -1
        } else if (value1 > value2) {
            return 1
        } else {
            return 0
        }
    }
}

let data = [
    {
        name: 'Zachary',
        age: 28
    },
    {
        name: 'Nicholas',
        age: 29
    }
]
data.sort(createCompareFunction('name'))
console.log(data, 'data');

10.9 函数对象

arguments.callee

n * factorial(n - 1)就是高耦合性

// 
function factorial (n) {
  if (n <= 1) {
    return 1
  }
  return n * factorial(n - 1)
}
console.log(factorial(5), 'factorial(5)'); // 120

改成如下,就解决了这个问题 arguments的callee属性指向arguments对象所在函数的指针

function factorial2 (n) {
    if (n === 1) {
        return 1
    }
    return n * arguments.callee(n - 1) 
}
console.log(factorial2(3)); // 6

如果是这样,trueFactorial2 的阶层也是 0,trueFactorial2 内部执行的是n * factorial(n - 1。而 factorial 已经是一个返回 0 的函数,

let trueFactorial2 = factorial
factorial = function () {
    return 0
}
console.log(factorial(3), 'trueFactorial2(3)'); // 0
console.log(trueFactorial2(3), 'trueFactorial2(3)'); // 0

但是如果是factorial2,用 arguments.callee 改写了就没关系

let trueFactorial3 = factorial2
factorial2 = function () {
    return 0
}
console.log(factorial(3), 'trueFactorial2(3)'); // 0
console.log(trueFactorial3(3), 'trueFactorial2(3)'); // 6

this

基本知识点和 10.1 节的一致,再次强调一下,事件监听或者定时回调里面的 this 如果指向 window,不是我们想要的,通过箭头函数修改就

caller

该属性返回,谁调用了当前函数,如下 outer 函数内执行 inner,所以 inner 的 caller 指向 outer;outer 的 caller 指向 null

function outer () {
    console.log(outer.caller); // 全局作用域中调用,指向null
    inner()
}
function inner () {
    console.log(inner.caller); // 会执行outer函数
}
outer()

使用 arguments.callee.caller能够降低耦合

function inner () {
    console.log(inner.caller); // 会执行outer函数
+    console.log(arguments.callee.caller); // 会执行outer函数
}

严格模式下始终报错,非严格模式下始终返回 undefined

function inner () {
    console.log(inner.caller); // 会执行outer函数
    console.log(arguments.callee.caller); // 会执行outer函数
+    console.log(arguments.caller); // 会执行outer函数
}
outer()

严格模式的其他注意点:

  1. 访问 inner.caller 也会报错
  2. 更别提给他赋值了

new.target 属性 ,若是 new 调用的,返回被调用的函数;否则返回 undefined

// 检测函数是否是用new调用的
function King () {
    if (!new.target) {
        throw 'King must be instantiated using "new"'
    }
    console.log('King instantiated using new');
}
new King() // "King instantiated using new"
King() // "King must be instantiated using "new"

10.10 函数的属性和方法

函数的 length 属性,返回函数形参的个数

function sum (num1) {
    return num1
}
function sum1 (num1, num2) {
    return num1
}
function sum2 () {
    return num1
}
console.log(sum.length, 'sum.length'); // 1
console.log(sum1.length, 'sum.length'); // 2
console.log(sum2.length, 'sum.length'); // 0

prototype 对象,保存引用类型所有实例的方法,函数也有,但是 for-in 不可遍历,因为他是不可枚举额度

console.log(sum2.prototype, 'sum2.prototype');

函数的 apply 方法、call 方法:

  • 他们第一个参数都是要修改的 this 值
  • apply 方法第二个参数传数组,call 参数要一个一个传递
// 函数的 apply 方法、call 方法
function sum3 (num1, num2) {
    return num1 + num2
}
function applySum3 (num1, num2) {
    return sum3.apply(this, arguments)
}
function callSum4 (num1, num2) {
    return sum3.call(this, ...arguments)
}
console.log(applySum3(1, 3), 'callSum3(1, 3)'); // 4
console.log(callSum4(1, 3), 'callSum3(1, 3)'); // 4

注意,严格模式,this 指向如果没有绑定,不指向 window,而是 undefiend

function globalThis () {
    "use strict"
    console.log(this, 'this');
}
globalThis()

call 和 apply 厉害的是他改变 this 的能力

window.color = 'red'
let o = {
    color: 'blue'
}
function sayColor () {
    console.log(this.color, 'this.color');
}
sayColor() // red
sayColor.call(this) // red
sayColor.call(wiundow) // red
sayColor.call(o) // blue

bind 方法 会返回一个新方法

let objectSayColor = sayColor.bind(o)
objectSayColor() // blue

10.11 函数表达式

这里知识点和上文提到的几乎类似,函数表达式没有函数的提升声明,必须先声明再调用

注意下面例子,下面这样写没有意义,因为存在提前声明,不同浏览器的效果会不一样

if (condition) {
    function sayHello () {
        console.log('Hi');
    }
} else {
    function sayHello1 () {
        console.log('sayHello1');
    }
}

用函数表达式可以解决

let fn;
if (condition) {
    fn = function () {
        console.log('Hi');
    }
} else {
    fn = fnunction () {
        console.log('sayHello1');
    }
}

10.12 递归

n * factorial(n - 1)就是高耦合性

// 
function factorial (n) {
  if (n <= 1) {
    return 1
  }
  return n * factorial(n - 1)
}
console.log(factorial(5), 'factorial(5)'); // 120

如下修改后,会出现报错,原因就是因为函数内部直接用函数名,而这个函数名被置空了

let newFactorial = factorial
factorial = null
newFactorial(5) // 会报错

改成如下,就解决了这个问题 arguments的callee属性指向arguments对象所在函数的指针

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

问题:如果是严格模式,arguments.callee 是不能访问,直接报错

命名函数表达式

"use strict";
// 改成命名函数表达式能够解决严格模式arguments.callee不能访问的问题
let factorial = (function f(num) {
    if (num <= 1) {
        return num
    }
    return num * f(num - 1)
})
let newFactorial = factorial;
factorial = null; 
console.log(newFactorial(5), 'newFactorial(5)');

10.13 尾调用

尾调用:函数的最后一步 的返回值 是调用另一个函数;

function gn(x) {
  return x + 1
}
function fn (x) {
  return gn(x)
}

如上 fn 最后一步是调用 gn 函数,这就是尾调用

不属于的情况:

// 尾调用
function fn (x) {
 return gn(x)
}

// 不属于 有赋值操作
function fn2 (x) {
  let y = gn(x)
  return y
}

// 不属于 有+法操作
function fn3 (x) {
  return 1 + gn(x)
}

// 不属于,没有返回值
function fn4 (x) {
  gn(x)
}

// 不属于尾调用,inner函数使用闭包,foo变量无法销毁,outer函数帧不能去掉
function outerFunction (a) {
  let foo = 'bar'
  function innerFunction (value) {
    return foo
  }
  return innerFunction(a)
}

尾调用不一定需要在最后一行,但需要在最后一步:

function f(x) {
  if (x > 0) {
    return gn(x)
  }
  return m(x)
}

如下也算,两个内部函数都在尾部

function outerFunction (condition) {
  return condition ? innerFunctionA() : innnerFunctionB()
}

函数调用栈:

  1. 函数调用会在内存中形成“调用记录”,又称作调用帧,用来保存变量的信息和函数调用的位置。
  2. 如果在函数 A 内部执行了函数 B,会形成 A 的调用帧,并且 A 的上方还有 B 的调用帧
  3. B 函数执行完毕返回到 A,B 的调用帧才会消失
  4. 调用帧形成了整个的调用栈

尾调用优化:

  1. 因为函数的最后一步就是函数的执行操作,所以不需要保存外部函数的调用信息了,只需要保存最后一个函数的帧信息
 function f() {
  let m = 1;
  let n = 2;
  return g(m + n)
 }
 // 等同于
 function f() {
  return g(3)
 }
 // 等同于
 g(3)

如上写法中,如果不是尾调用,就需要保存 m、n 的变量,和 f 的调用信息,有了尾调用,就只需要最后保留 g(3)的信息即可

递归优化--> 尾递归

function factorial (n) {
    if (n <= 1) {
        return 1
    }
    return n * factorial(n - 1)
}
console.log(factorial(5), 'factorial(5)'); // 120 

优化后

 // 改写递归函数为尾调用 => 就是尾递归
 // 时间复杂度是O(n) 空间复杂度O(1),如果所在环境的JS引擎支持尾调用优化,则为O(1), 否则就是O(n)
 function factorial2 (n, total) {
      if (n <= 1) {
          return total
      }
      return factorial2(n - 1, n * total)
 }
 console.log(factorial2(5, 1), 'factorial(5)'); // 120

但是这个阶乘函数,执行要传两个参数很奇怪,改写后

// 改写递归函数,这样执行factorial3的时候,就只用传递一个参数了
function factorial3 (n) {
  return tailFactorial(n, 1)
}

function tailFactorial (n, total) {
  if (n === 1) return total
  return tailFactorial(n - 1, n * total)
}
console.log(factorial3(5), '改写后的递归函数');

函数柯里化,将多参数的函数转化为单参数的形式==> factorial4 函数执行时,只用传递一个参数

function tailFactorial (n, total) {
    if (n === 1) return total
    return tailFactorial(n - 1, n * total)
}
console.log(factorial3(5), '改写后的递归函数');

// 函数柯里化
function currying (fn, n) {
    return function (m) {
        return fn.call(this, m, n)
    }
}
const factorial4 = currying(tailFactorial, 1)
console.log(factorial4(5), 'factorial4(5)');

斐波那契数列,优化前

// 计算斐波那契数列
function feibo (n) {
    if (n <= 1) {
        return 1
    }
    return feibo(n - 1) + feibo(n - 2)
}

优化后

function feibo2 (n, ac1 = 1, ac2 = 1) {
    if (n <= 1) return ac2
    return feibo2(n - 1, ac2, ac1 + ac2)
}
// console.log(feibo2(3), 'feibo2(100)');
// console.log(feibo2(100), 'feibo2(100)');
// console.log(feibo2(1000), 'feibo2(1000)');
// console.log(feibo2(10000), 'feibo2(10000)');

注意,尾调用执行时,函数的调用栈会改写,所以:

  1. 尾调用必须在严格模式下执行
  2. fn.arguments 变量和 fn.caller 变量会失真,

正常模式,如何实现递归呢?如下借助蹦床函数,注意是返回一个函数,继续执行,而不是在函数里面执行一个函数

// 蹦床函数
function trampoline (f) {
    // 函数f执行后,返回的还是函数,继续执行
    while (f && f instanceof Function) {
        f = f()
    }
    return f
}


function sum2 (x, y) {
    if (y > 0) {
        // sum2.bind 返回一个函数,而不是直接执行sum2
        return sum2.bind(null, x + 1, y - 1)
    } else {
        return x
    }
}
// 利用蹦床函数
console.log(trampoline(sum2(1, 100000))); // 100001

真正的尾调用的实现

function tco(f) {
    var value;
    var active = false;
    var accumulated = [];

    return function accumulator() {
        accumulated.push(arguments);
        if (!active) {
            active = true;
            while (accumulated.length) {
                debugger
                value = f.apply(this, accumulated.shift());
            }
            active = false;
            return value;
        }
    };
}

var sum = tco(function(x, y) {
    if (y > 0) {
        // 执行sum其实是在执行 accumulator
        return sum(x + 1, y - 1)
    }
    else {
        return x
    }
});
console.log(sum(1, 100000), 'sum(1, 100000)');
// 100001

10.14 闭包

闭包是一个函数,他引用了另一个函数作用域中的变量,如下代码中,内层匿名函数引用了createCompareFunction 函数的propertyName 变量,这个匿名函数是闭包函数

function createCompareFunction (propertyName) {
    return function (obj1, obj2) {
        let value1 = obj1[propertyName]
        let value2 = obj2[propertyName]

        if (value1 < value2) {
            return -1
        } else if (value1 > value2) {
            return 1
        } else {
            return 0
        }
    }
}

const compare = createCompareFunction('name')
let result = compare(5, 10)

注意,闭包函数引用了其他函数作用域的变量,该变量在createCompareFunction 函数执行完毕后,也不会被销毁,因为引用关系仍旧存在


把闭包函数置空,就可以销毁 propertyName 变量

// 这样就能够销毁
compare = null 

执行上下文、作用域链、活动对象,这些概念有什么关系:

  1. 调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链
  2. 执行上下文是函数执行时的环境,包括作用域链,作用域链会指向当前函数的活动对象/变量对象(活动对象包括变量和函数),也会指向外部上下文的变量环境

10.14.1 闭包中的 this 指向

函数调用时会创建 this 和 arguments 这两个对象

如下函数调用时,内部函数无法直接访问到上一层作用域的 thisarguments 对象,所以闭包函数执行时,this 指向 window

window.identity = 'The window'
let object = {
    identity: 'My Object',
    getIdentityFunc () {
        console.log(this, 'this 14'); // 打印的是对象
        return function () {
            console.log(this, 'this 16'); // 打印的是window
            return this.identity
        }
    }
}
console.log(object.getIdentityFunc()()); // The Window

修改: 把 this 存到 that 里面,能直接访问

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

此外,注意如下三种情况的执行,注意第三个,赋值后就是普通函数的身份调用了

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

10.14.2 内存泄漏

闭包函数存在内存泄漏的情况

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

必须把 element 置空

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

10.15 立即调用的函数表达式

立即调用表达式,又称作为 IIFE,类似于函数声明,但是被包含在括号中。

如下会报错,因为 IIFE 里面是块级作用域

(function () {
    // 块级作用域
    for (var i = 0; i < 5; i++) {
        console.log(i, 'i');
    }
})()
console.log(i, 'i');

这样打印都是 2

<div>1</div>
<div>2</div>
<script>
  (function () {
      // 块级作用域
  })()

  // (function () {
  //     // 块级作用域
  //     for (var i = 0; i < 5; i++) {
  //         console.log(i, 'i');
  //     }
  // })()
  // console.log(i, 'i');


  let divs = document.querySelectorAll('div')
  for (var i = 0; i < divs.length; i++) {
      divs[i].addEventListener('click', function () {
          console.log(i);
      })
  }

</script>

这样修改后,点击,打印是正常的

// 这样修改后点击,打印是正常的
for (var i = 0; i < divs.length; i++) {
    divs[i].addEventListener('click', (function (value) {
        return function () {
            console.log(value, 'value');
        }
    })(i))
}

不过,现在用 let,或者 {}内嵌块级作用域,都可以解决这些问题了,但是注意,如下还是会出现问题。原因还是闭包捕获了原始变量

let i;
for (i = 0; i < divs.length; i++) {
    divs[i].addEventListener('click', function () {
        console.log(i);
    })
}

10.16 私有变量

私有变量:任何定义在函数或块中的变量,都可以认为是私有的,外部无法访问这个块里面的鼻梁。

这个函数中有三个私有变量,num1,num2,sum

// 私有变量
function add (num1, num2) {
  let sum = sum1 + sum2
  return sum
}

特权方法:访问函数私有变量的公有方法。一种是在构造函数上实现。如下代码中,privateMethod 方法定义在原型链上,是闭包,所以能够访问到privateVariable 变量和 privateFunction 函数,外部是没有办法访问到这俩,他们不是定义在实例上。

function myObject () {
    let privateVariable = 10;

    function privateFunction () {
        return false
    }

    // 特权方法
    this.privateMethod = function () {
        privateVariable++
        return privateFunction()
    }
}

这两都是闭包函数,能够访问到 name 值

// 创建两个特权方法
function Person (name) {
  this.getName = function () {
    return name
  }
  this.setName = function (value) {
    name = value
  }
}
let person = new Person('Mike')
console.log(person.getName(), 'person.getName'); // Mike
person.setName('John')
console.log(person.getName(), 'person.getName'); // John

10.16.1 静态私有变量

特权方法,通过私有作用域定义私有变量和函数

如下利用匿名函数表达式,注意MyObject 是一个全局变量,没有使用关键字声明。privateVariable 变量和privateFunction 函数均是由实例共享的

(function () {
    let privateVariable = 10;
    function privateFunction () {
        return false
    }

    MyObject = function () {}

    MyObject.prototype.publicMethod = function () {
        privateVariable++
        return privateFunction()
    }
})()

let myObj1 = new MyObject()
myObj1.publicMethod() // 11
let myObj2 = new MyObject()
myObj2.publicMethod() // 12

注意,严格模式给未声明的变量赋值会报错

如下例子,setName 后,person1 和 person2 的 getName 都发生了变化,他们访问的是同一个 name 值

// 私有作用域
(function () {
    let name = '';

    Person = function (value) {
        name = value
    }

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

注意:

  1. 使用闭包和私有变量,会导致原型链变长,原型链越长,查找变量所需要的时间就越长

10.16.2 模块模式

如下创建匿名函数,然后创建私有的变量和私有函数,返回一个对象,在这个对象中返回对私有变量和函数的引用

// 模块模式1
let singleton = function () {
    let privateVariable = 10;
    function privateFunction () {
        return false
    }

    return {
        publicProperty: true,
        publicMethod() {
            privateVariable++
            return privateFunction()
        }
    }
}

如下应用中,返回的 get 方法用于访问组件,register 方法用于注册组件

// 模块模式2
let application = function () {
    let components = new Array()
    components.push(new BaseComponent())

    return {
        getComponent() {
            return components
        },
        registerComponent (component) {
            if (typeof component === 'object') {
                components.push(component)
            }
        }
    }
}

如何增强呢?单例对象必须是某个特定类的实例,又要给他添加额外的属性和方法

第一个示例的增强写法:

// 模块模式1的增强写法
let singleton1 = function () {
    let privateVariable = 10;
    function privateFunction () {
        return false
    }

    let object = new CustomType();

    object.publicProperty = true;
    object.publicMethod = function () {
        privateVariable++
        return privateFunction()
    }
    return object
}

第二个示例的增强写法:

// 模块模式2的增强写法
let application1 = function () {
    let components = new Array()
    components.push(new BaseComponent())

    let app = new BaseComponent()
    app.getComponent = function () {
        return components
    }
    app.registerComponent = function () {
        if (typeof component === 'object') {
            components.push(component)
        }
    }
    return app
}