函数的定义方式
函数声明
function add(a, b) {
return a + b;
}
函数声明的最重要的一个特征是函数声明提升,意思是在执行代码之前会先读取函数声明,所以函数声明可以放在调用函数语句之后。
add(1, 2);
function add(a, b) {
return a + b;
}
上述代码可以正常执行,不会报错。
函数表达式
// 普通的函数表达式
let add = function(a, b) {
return a + b;
}
add(1, 2)
// 函数表达式也可以有函数名,这个函数名不能在函数外面用,只能在函数内部用
let fn = function newFn(){
console.log(newFn); // 可以在这里面用。有一个作用就是在这里用递归
};
fn(); // 可以执行
newFn(); // 报错,不能在外面用
函数表达式不能进行函数提升
add(1, 2); // 在这里调用会报错
var add = function(a, b) {
return a + b;
}
add(1, 2); // 3
函数声明与函数表达式的区别
-
函数声明会被提升到当前作用域的顶部,函数表达式则不会。
-
函数声明一定会有函数名,而函数表达式一般不会有函数名(也可以有)。
-
函数声明不是一个完整的语句,所以不能出现在
if-else、for 循环、finally、try catch语句、with语句中,而函数表达式则可以。 -
函数有一个
name属性,指向紧跟在function关键字之后的那个函数名。如果函数表达式没有名字,那name属性指向变量名。// 函数的name属性 function a() { console.log(a.name); // a } a(); console.log(a.name); // a ----- let b = function () { console.log(b.name); // b }; b(); console.log(b.name) // b ----- let c = function temp() { console.log("c.name:", c.name); // temp console.log("temp.name:", temp.name); // temp }; c(); console.log("c.name:", c.name); // c console.log("temp.name:", temp.name); // temp is not defined
箭头函数
let add = (a, b) => {
return a + b;
}
基本语法是:
参数 => 函数体
(参数) => { 函数体 }
注意点:
-
箭头函数是不能提升的,所以需要在使用之前定义。
-
使用 const 比使用 var 更安全,因为函数表达式始终是一个常量。
-
如果函数部分只是一个语句,则可以省略 return 关键字和大括号 { },这样做是一个比较好的习惯
特点:
-
有一个形参可以省略小括号
-
函数体中 return 后只有一条语句 可以省略 return 和 {}
-
箭头函数没有 arguments 内置对象
-
箭头函数不能用作构造函数
-
箭头函数没有原型属性
-
箭头函数的 this 指向父作用域(定义它的地方)
// 普通函数
var f = function(a){
return a * 10;
}
f(1); // 10
// 箭头函数,只有 1 个参数可以省略小括号
var f = a => a * 10
f(1); // 10
// 当箭头函数没有参数或者有多个参数,要用 () 括起来。
var f1 = () => a + b;
var f2 = (a, b) => a + b;
f2(6,2); //8
// 当箭头函数函数体有多行语句,用 {} 包裹起来
var f = (a, b) => {
let result = a + b;
return result;
}
f(6,2); // 8
// 当箭头函数要返回对象的时候,为了区分于代码块,要用 () 将对象包裹起来
var f = (id,name) => ({id: id, name: name});
f(6,2); // {id: 6, name: 2}
自执行函数
自执行函数也叫立即调用的函数表达式(IIFE)。它的作用为我们不用主动地去调用函数,它会自己调用。
IIFE的几种写法:
// 基本的IIFE写法:
(function() {
// 代码块
})();
// 先写两组小括号,再在第一个小括号内写匿名函数
// 基本写法2,函数名fn可要可不要
(function fn(){
console.log('函数声明执行');
}());
// 将参数传递给IIFE的写法:
(function(params) {
console.log('Hello ' + params);
})('Alice');
// 定义一个变量来存储IIFE的返回值:
var result = (function() {
// 计算结果
return 'Hello World';
})();
console.log(result);
// 注意下面这种写法,它也是IIEF:
!function fn() {
console.log(1);
}();
/*
* function fn() {
* console.log(1);
* }
* 这是一个函数声明,
* 前面加上 ! 运算符会变成函数表达式,
* 接着,在函数表达式后面加上 (),表示立即调用该函数。
* 不是只能用 ! ,还可以用:
* ~
* +
* -
* 0+
* true&&
* false||
* 等等...
*/
构造函数
使用构造函数定义函数,基本语法是:
定义:var 函数名 = new Function("参数列表","函数体");
调用:函数名 ();
var add = new Function('a', 'b', 'return a + b;');
console.log(add(1, 2)); // 输出 3
需要注意的是,我们通常不直接使用 new Function 构造函数来定义函数,因为它有着性能慢、具有安全风险、可读性差等缺点。
函数的一些概念
函数作用域
在 JavaScript 中,函数具有自己的作用域,函数内部声明的变量在函数外部不可见,称为"函数作用域"。
闭包
JavaScript 允许内部函数(函数声明和函数表达式)位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
function A() { // 外部函数
let count = 0; // 外部函数中的变量
function B() { // 内部函数
count++; // 内部函数访问外部函数的变量
console.log(count)
};
return B
}
let a = A(); // 内部函数在外部函数之外被引用,闭包形成
a(); // 调用闭包函数,输出 1
a(); // 调用闭包函数,输出 2
// 闭包 B 的引用 a、变量 count 它们会一直保存在内存中,无法被垃圾回收机制回收。
// 程序执行到这里,函数 A 的执行上下文会被销毁,但是函数 A 任然存在于内存中,
// 因为该函数在该示例中是存在于全局作用域中的,
// 只要没有显示删除它,就会一直在内存中。
// 如果想释放 B 和 count 的内存,要加一句代码:
a = null;
// 此时,没有其他引用指向闭包函数 B,它将成为垃圾回收的目标
简单来说:函数 A 内部有一个函数 B,函数 B 引用了函数 A 中的变量,函数 B 在函数 A 外部被引用,那么函数 B 就是闭包。
为什么闭包要被 return ?
因为如果闭包没有被 return,那就不会创建一个可以被外部访问的闭包实例,也就是说,函数 A 内部的变量只能在函数内部使用,永远无法被外部读取。
什么是执行上下文?为什么执行上下文被销毁了函数却依然存在?
函数的执行上下文是函数执行时所需的一个抽象环境,它包含了函数执行期间的所有信息,如参数、局部变量、this 值以及任何函数声明的环境等。每当一个函数被调用时,JavaScript 引擎都会为该函数创建一个新的执行上下文,并将其推入调用栈(也称为执行栈)的顶部。当函数执行完毕后,其执行上下文就会从调用栈中弹出并被销毁,同时该函数执行期间创建的所有局部变量和参数等也会随之被销毁(除非它们被闭包捕获)。
然而,函数的定义(即函数体本身)并不包含在执行上下文中。函数的定义是存储在内存中的一个固定部分,这个定义是持久的,意味着只要它仍然可以被访问到(例如,通过函数名或其他引用),它就会保留在内存中。
因此,当函数的执行上下文被销毁时,只是表示该函数的一次特定执行结束了,相关的局部变量和参数等不再可用。但这并不影响函数定义的持久性,因为函数定义本身并不依赖于任何特定的执行上下文。实际上,函数定义可以被多次调用,每次调用都会创建一个新的执行上下文来执行函数体中的代码。
总结来说,函数的执行上下文是函数执行时所需的一个临时环境,它包含了函数执行期间的所有信息(参数、局部变量、this 等),并在函数执行完毕后被销毁。而函数的定义是持久的,它描述了函数应该如何执行,并可以在需要时被多次调用以创建新的执行上下文。
闭包的作用:
-
可以读取外部函数内部的变量
-
让这些变量的值始终保持在内存中
-
数据封装和私有变量
- 私有变量的意思是:使变量的作用域只在函数内部有效,防止被外部破坏
闭包的缺点:
从最开始的示例就可以知道,闭包的缺点就是容易导致内存泄漏,要注意显示地删除闭包地引用。
实际开发中哪些场景使用了闭包
-
数据封装和私有变量
function createPerson(name) { let _name = name; // 私有变量 return { getName: function() { return _name; }, setName: function(newName) { _name = newName; } }; } const person = createPerson('Alice'); console.log(person.getName()); // 输出: Alice person.setName('Bob'); console.log(person.getName()); // 输出: Bob -
异步函数
function asyncGreeting(name) { let age = 1; setTimeout(function() { // 闭包 age++; console.log(`我叫 ${name}, 我 ${age}岁了`); }, 1000); } asyncGreeting('Alice'); // 我叫 Alice, 我 2 岁了 asyncGreeting('Alice'); // 我叫 Alice, 我 2 岁了, 没有变化注意这段代码并不是一个完整功能的闭包,因为这里的闭包函数不能被 asyncGreeting 函数外部访问,这里的闭包的作用仅仅是访问了外部函数(asyncGreeting 函数)的参数和变量。
-
柯里化
柯里化是将复杂的函数拆分为多个简单的函数,每个函数只处理一个参数,并返回一个新的函数,直到所有参数都被处理并返回最终结果。从而增加函数的可读性和可维护性。
// 原代码 function add(x, y, z) { x = x + 2 y = y * 2 z = z * z return x + y + z } console.log(add(10, 20, 30)) // 柯里化 function sum2(x) { x = x + 2 return function(y) { y = y * 2 return function(z) { z = z * z return x + y + z } } } console.log(sum2(10)(20)(30)) // 简化柯里化 var sum3 = x => y => z => { return x + y + z } console.log(sum3(10)(20)(30)) var sum4 = x => y => z => x + y + z console.log(sum4(10)(20)(30))
arguments 对象
arguments 是一个数组对象,它表示传递给一个函数的参数列表。在 ES6 之前的版本中,当你不确定函数会接收多少个参数,或者你想以一种动态的方式处理参数时,arguments 对象非常有用。
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(1,2,3)); // 输出6
rest 参数
在 JavaScript 中,rest 参数(也称为剩余参数或剩余形参)是一种允许你将一个不定数量的参数表示为一个数组的特殊语法。它使用三个点(...)作为前缀,后面跟着一个参数名。这个参数名实际上是一个数组,包含了函数被调用时传入的所有剩余参数。
rest 参数的使用非常灵活,特别是在你不知道会有多少参数传入函数,或者你想将参数列表的一部分作为一个数组来处理时。
function sum(...numbers) {
let total = 0;
for (let number of numbers) {
total += number;
}
return total;
}
console.log(sum(1,2,3)); // 输出6
函数中的 this
普通函数中的 this
-
当函数作为普通函数被调用时(即不是作为对象的方法、不是构造函数、不是使用 call/apply/bind 方法调用),this 通常指向全局对象(在浏览器中是 window)。在严格模式(strict mode)下,this 是 undefined。
function myFunction() { console.log(this === window); // 通常输出 true,但在严格模式下输出 false } myFunction(); -
当函数作为对象的方法被调用时,this 指向调用该方法的对象。
const myObject = { value: 'Hello', myMethod: function() { console.log(this.value); // 输出 'Hello' } }; myObject.myMethod();
箭头函数中的 this
箭头函数的 this 总是引用定义它的那个上下文(即包含它的函数或全局上下文)中的 this 值。
function Person() {
this.name = 'a';
this.age = 10;
let that = this;
console.log("this1:",this); // {name: 'a', age: 10}
setInterval(()=>{
console.log("this2:",this); // {name: 'a', age: 10}
},1000)
setInterval(function() {
console.log("this3:",this); // Window
console.log("this4:",that) // {name: 'a', age: 10}
},1000)
}
let p = new Person()
构造函数中的 this
构造函数的 this 用于在创建新对象时引用该对象本身。
间接调用模式中的 this
使用 Function.prototype.call、Function.prototype.apply 或 Function.prototype.bind 方法可以显式设置 this 的值。
function greet(greeting, punctuation) {
return greeting + ', ' + this.name + punctuation;
}
const obj = { name: 'World' };
console.log(greet.call(obj, 'Hello', '!')); // 输出 'Hello, World!'
console.log(greet.apply(obj, ['Hello', '!'])); // 输出 'Hello, World!'
const boundGreet = greet.bind(obj);
console.log(boundGreet('Hello', '!')); // 输出 'Hello, World!'
call、apply、bind
这 3 者的作用:改变函数执行时内部 this 的指向。
为什么需要改变
this的指向?
例如当一个函数 A 作为回调函数传递时,那么函数 A 中的 this 的指向就有可能不是你预期的,就比如说回调函数被 setTimeout、setInterval 等调用,那么这时候 this 的指向可能是全局对象或 undefined;
再例如下面这个例子:
function test(param1, param2) {
console.log('性别:' + this.sex + ';' + param1 + ';' + param2)
}
const man = {
sex: '男',
age: 10
}
const woman = {
sex: '女',
age: 10
}
有两个不同的对象都想调用 test 方法,该怎么做呢?你现在只能将这个函数放到对象中:
const man = {
sex: '男',
age: 10,
test: function(param1, param2) {
console.log('性别:' + this.sex + ';' + param1 + ';' + param2)
}
}
const woman = {
sex: '女',
age: 10,
test: function(param1, param2) {
console.log('性别:' + this.sex + ';' + param1 + ';' + param2)
}
}
man.test() // 性别:男;undefined;undefined
如果你会使用 call 或 apply 或 bind,就很简单了:
function test(param1, param2) {
console.log('性别:' + this.sex + ';爱好1:' + param1 + ';爱好2:' + param2)
}
const man = {
sex: '男',
age: 10
}
const woman = {
sex: '女',
age: 10
}
const other = {
sex: '其他',
age: 10
}
test.call(man) // 性别:男;爱好1:undefined;爱好2:undefined
test.apply(woman) // 性别:女;爱好1:undefined;爱好2:undefined
const bindFn = test.bind(other)
bindFn() // 性别:其他;爱好1:undefined;爱好2:undefined
所以说它们的作用就是改变函数执行时内部 this 的指向。
那它们三个的区别是什么呢?
-
call 和 apply 都会立即调用函数,bind 不会立即执行函数,bind 会返回一个新的函数。就比如上面的例子,
bindFn是一个函数,需要手动去执行bindFn(),而 call 和 apply 就不需要手动去执行。 -
它们的第一个参数都是 this 的指向,第二个参数传递有区别:
-
call:按顺序依次传递参数
test.call(man, '游泳') // 性别:男;爱好1:游泳;爱好2:undefined test.call(man, '游泳', '健身') // 性别:男;爱好1:游泳;爱好2:健身 // 超出的参数不处理 test.call(man, '游泳', '健身', '玩球') // 性别:男;爱好1:游泳;爱好2:健身 -
apply:接收一个数组
test.apply(man, ['游泳']) // 性别:男;爱好1:游泳;爱好2:undefined test.apply(man, ['游泳', '健身']) // 性别:男;爱好1:游泳;爱好2:健身 // 超出的参数不处理 test.apply(man, ['游泳', '健身', '玩球']) // 性别:男;爱好1:游泳;爱好2:健身 // [] 也可以改成 null 或 undefined,是一样的结果 test.apply(man, []) // 性别:男;爱好1:undefined;爱好2:undefined test.apply(man, '游泳') // 报错,只能接收数组 -
bind:与 call 相同也是按顺序依次传递参数,与 call 不同的是 bind 返回的新函数也可以接收参数
const bindFn1 = test.bind(other, '游泳') bindFn1() // 性别:其他;爱好1:游泳;爱好2:undefined // 两个参数合并了 const bindFn2 = test.bind(other, '游泳') bindFn2('健身') // 性别:其他;爱好1:游泳;爱好2:健身 const bindFn3 = test.bind(other) bindFn3('游泳', '健身') // 性别:其他;爱好1:游泳;爱好2:健身 const bindFn4 = test.bind(other, '游泳', '健身') bindFn4() // 性别:其他;爱好1:游泳;爱好2:健身 // 超出的参数不处理 const bindFn5 = test.bind(other, '游泳', '健身') bindFn5('玩球') // 性别:其他;爱好1:游泳;爱好2:健身
-
回调函数
什么是回调函数
把函数 B 当作参数传递给函数 A,当函数 A 在某个特定时刻调用函数 B, 那么函数 B 就是回调函数。
// 主函数
function A(callback) {
callback() // 回调函数执行
}
// 回调函数
function B() {
console.log("我是回调函数")
}
A(B)
// ---分割线---
// 箭头函数简写
function A(callback) {
callback()
}
A(()=> {
console.log("我是回调函数")
})
// 主函数
function A(params, callback) {
callback(params) // 回调函数执行
}
// 回调函数
function B(params) {
console.log("我是回调函数")
console.log("我接收来自主函数的参数:" + params)
}
A('呵呵', B)
// ---分割线---
// 箭头函数简写
function A(params, callback) {
callback(params)
}
A('呵呵', params=> {
console.log("我是回调函数")
console.log("我接收来自主函数的参数:" + params)
})
回调函数的作用
-
作为参数
回调函数最大的作用就是把函数作为参数传递
function A(name, myCallback) { myCallback(name); } // 函数1 function sayHello(name) { console.log(`Hello, ${name}!`); } // 函数2 function sayHi(name) { console.log(`Hi, ${name}!`); } // 将函数作为参数传递 A('Alice', sayHello); // 输出: Hello, Alice! A('Bob', sayHi); // 输出: Hi, Bob! -
异步编程
可以在一个异步函数执行完毕之后,再执行另一个函数(回调函数)
function A(params, callback) { // 模拟异步函数 setTimeout(()=>{ let result = params + '数据结果'; callback(result) // 调用回调函数 // 还可以做些其他操作 }, 1000) } function B(params) { // do something console.log('回调函数接收结果:', params) } A('哈哈', B) console.log('主线程运行中...') //主线程运行中... // ... 等待 1 秒 // 回调函数接收结果: 哈哈数据结果为什么
function A(params, callback)中的 callback 不接收参数,callback(result)却可以接收参数呢?因为第 1 个 callback 其实是一个函数引用,它指向了函数 B,它并非真正的函数。第 2 个 callback 才是一个可以接收参数的真正函数。
上述代码也可以简写为:
function A(params, callback) { // 模拟异步函数 setTimeout(()=>{ let result = params + '数据结果'; callback(result) // 调用回调函数 // 还可以做些其他操作 }, 1000) } A('哈哈', params => { console.log('回调函数接收结果:', params) }) console.log('主线程运行中...') -
事件监听
我们在使用
addEventListener函数时就经常用到了回调函数document.getElementById('myButton').addEventListener('click', function() { console.log('按钮被点击了!'); // 这个匿名函数就是回调函数,它在点击事件发生时被调用 }); // 主线程继续执行,不会等待点击事件发生 console.log('主线程运行中...');
以上都是回调函数的一些表面作用。回调函数还有一个隐藏的作用就是:当我们传递一个回调函数作为参数时,我们只需关注如何在特定的条件下调用这个回调函数,而无需关心它内部的具体实现细节。这是什么意思呢?
let promise = new Promise((resolve, reject)=> {
if(true) {
resolve('成功')
}
});
在这段代码中,resolve 其实就是 Promise 的构造函数的一个回调函数,当我们调用 resolve('成功') 时,该回调函数会将 promise 的状态从 pending 变为 fulfilled,这个过程是在 resolve 回调函数内部执行的,调用者无需关心内部的具体细节。
回调函数的优缺点
-
优点:
-
解耦:回调函数允许我们将任务的执行与任务完成后的操作分离
-
灵活:可以根据需要传递不同的函数作为回调函数
-
-
缺点:
-
回调地狱:当多个异步操作需要顺序执行,并且每个操作都依赖于前一个操作的结果时,就会导致嵌套的回调函数层级过深,使得代码难以阅读和维护。
-
错误处理:在多层嵌套的回调函数中,错误处理会变得复杂
-
针对回调函数的缺点有哪些解决办法?