本文介绍《JavaScript高级程序设计》的10章-函数,看完你将能回答如下问题:
- 声明一个函数有几种方式?【10.0】
- 函数声明和函数表达式有区别吗?【10.7】
- 箭头函数和普通函数有什么区别?【10.1 2】
- 函数重载【10.4】
- 了解arguments.callee对象吗?了解arguments.callee.caller吗?【10.9】
- 了解new.target属性吗?【10.9】
- 函数的方法 call apply bind区别?【10.10】
- 了解函数的尾调用吗?【10.13】
- 了解函数的闭包吗?闭包的优缺点是什么?【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 箭头函数语法
- 函数声明:
let fn1 = (a, b) => {
return a + b
}
// 等同于
let fn2 = function (a, b) {
return a + b
}
- 参数是否需要圆括号:
// 不需要参数或需多个参数,使用(),
let fn3 = () => {
return 1
}
let fn4 = (a, b) => {
return a + b
}
// 一个参数可以省略圆括号
let fn5 = x => {
return x * 3
}
- 如果代码里面超过一行代码,需要使用大括号:
let fn6 = (a) => {
let b = a + 3
return b
}
- 当只有一行代码时,不需要大括号,且会默认返回箭头后面的值:
let fn7 = (a) => a + 3
5. 返回对象的错误写法:
let fn8 = (a) => {a: 1}
console.log(fn8(), 'fn8()'); // undefined
- 返回对象应该增加圆括号在外面:
let fn9 = () => ({a: 1})
console.log(fn9(), 'fn9()'); // {a: 1} 'fn9()'
- 不需要返回值时这样写,用
void关键字:
// 不需要任何返回的情形
let fn10 = () => void 222
console.log(fn10(), 'fn10()'); // undefined
- 可以和解构赋值配合起来使用,从对象中
解构出num1和num2的属性值
const fn11 = ({num1, num2}) => {
return num1 + num2
}
console.log(fn11({num1: 1, num2: 2}), 'fn(11)'); // 3
- 和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, 2, 3].map(item => {
return x * x
});
// 箭头函数的写法
[1, 2, 3].map(() => x * x)
2 箭头函数注意事项
- 箭头函数不能使用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
- 不可以使用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)
- 不可以使用yield命令
- 箭头函数没有自己的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 理解参数
- 假设函数定义了 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
严格模式下:
- arguments[1] 被修改后,num2 还是之前的值
- 重写 arguments 对象会报错
// ES的函数名就是指向函数的指针
function fn3 (num1, num2) {
"use strict"; // 当前函数开启严格模式
arguments[1] = 10
return arguments[0] + num2
}
console.log(fn3(1, 2)); // 3
注意:
- 箭头函数无法访问到 arguments 对象
- 可以通过包装函数把参数给到箭头函数
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'));
形参和实参不匹配的情况:
- 形参和实参正好匹配,正常执行
- 实参多于形参,只取到形参的个数去执行
- 如果实参的个数小于形参的个数,多出来的形参会被定义未undefined,然后最终的结果就是NaN。
- 一个参数声明了没有赋值,输出它,就是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()
严格模式的其他注意点:
- 访问 inner.caller 也会报错
- 更别提给他赋值了
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()
}
函数调用栈:
- 函数调用会在内存中形成“调用记录”,又称作调用帧,用来保存变量的信息和函数调用的位置。
- 如果在函数 A 内部执行了函数 B,会形成 A 的调用帧,并且 A 的上方还有 B 的调用帧
- B 函数执行完毕返回到 A,B 的调用帧才会消失
- 调用帧形成了整个的调用栈
尾调用优化:
- 因为函数的最后一步就是函数的执行操作,所以不需要保存外部函数的调用信息了,只需要保存最后一个函数的帧信息
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)');
注意,尾调用执行时,函数的调用栈会改写,所以:
- 尾调用必须在严格模式下执行
- 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
执行上下文、作用域链、活动对象,这些概念有什么关系:
- 调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链
- 执行上下文是函数执行时的环境,包括作用域链,作用域链会指向当前函数的活动对象/变量对象(活动对象包括变量和函数),也会指向外部上下文的变量环境
10.14.1 闭包中的 this 指向
函数调用时会创建 this 和 arguments 这两个对象
如下函数调用时,内部函数无法直接访问到上一层作用域的 this 和 arguments 对象,所以闭包函数执行时,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
注意:
- 使用闭包和私有变量,会导致原型链变长,原型链越长,查找变量所需要的时间就越长
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
}