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
为什么函数声明可以提升,函数表达式却不能提升?
函数声明提升的原因是 JavaScript 在编译阶段会先扫描整个代码,并将所有的函数声明(即 function 关键字定义的函数)提升到作用域的顶部。这一过程发生在代码执行之前,因此你可以在函数声明之前调用该函数。
函数表达式本质上是一个赋值语句,在执行到该语句时才会进行求值和赋值操作。在编译阶段,只会对变量进行提升,但不会对赋值表达式中的函数进行提升。变量提升只是将变量的声明提升到作用域顶部,而变量的赋值操作还是在原来的位置执行。
console.log(add); // 不会报错
add(1, 2); // 报错
var add = function(a, b) {
return a + b;
}
函数声明与函数表达式的区别:
函数声明会被提升到当前作用域的顶部,函数表达式则不会。
函数声明一定会有函数名,而函数表达式一般不会有函数名(也可以有)。
函数声明不是一个完整的语句,所以不能出现在 if-else
、for 循环
、finally
、try catch
语句、with
语句中,而函数表达式则可以。
if (true) {
function test() { // 现代浏览器其实是可以执行的,但是严格模式会报错
console.log("Hello");
}
}
-----
if (true) {
var test = function() { // 正确:函数表达式可以出现在 if 语句中
console.log("Hello");
};
}
test(); // 输出: Hello
函数有一个 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 内置对象
箭头函数不能用作构造函数
箭头函数没有原型对象,即没有 prototype 属性
箭头函数的 this 指向父作用域(定义它的地方)
// 普通函数
const f = function(a){
return a * 10;
}
f(1); // 10
// 箭头函数,只有 1 个参数可以省略小括号
const f = a => a * 10
f(1); // 10
// 当箭头函数没有参数或者有多个参数,要用 () 括起来。
const f1 = () => a + b;
const f2 = (a, b) => a + b;
f2(6,2); //8
// 当箭头函数函数体有多行语句,用 {} 包裹起来
const f = (a, b) => {
let result = a + b;
return result;
}
f(6,2); // 8
// 当箭头函数要返回对象又不想写 return 的时候,可以用 () 将对象包裹起来
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);
// 给 IIFE 传参或使用变量存储虽然可行但是没有必要。
// 注意下面这种写法,它也是 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 中,函数具有自己的作用域,函数内部声明的变量在函数外部不可见,称为"函数作用域"。
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
注意:arguments 不是数组,它是一个类数组对象,不能直接使用数组的方法,如 forEach、map 等。
上面说到是在 ES6 之前,你在不确定参数数量的情况下动态处理参数可以用arguments 对象,那么在 ES6 之后呢?更优雅的方式出现了, 它就是 rest 参数,也叫剩余参数。
function sum(...numbers) {
let total = 0;
for (let number of numbers) {
total += number;
}
return total;
}
console.log(sum(1,2,3)); // 6
function sum(first, ...rest) {
let total = first;
for (let num of rest) {
total += num;
}
return total;
}
console.log(sum(1, 2, 3)); // 6
执行上下文主要有三种类型:全局执行上下文、函数执行上下文和 eval 执行上下文(eval 使用较少)。
全局执行上下文:在脚本开始执行时就会被创建,且在整个脚本执行期间只有一个全局执行上下文。
函数执行上下文:每当一个函数被调用时,JavaScript 引擎都会为该函数创建一个新的执行上下文,并将其推入调用栈(也称为执行栈)的顶部。
创建阶段,会进行:
变量提升(函数声明提升优先于变量声明提升)
创建词法环境:用于存储 let 和 const 声明的变量,以及块级作用域
外部环境引用:指向外部词法环境,用于实现作用域链,这是闭包实现的基础
确定 this 的值:具体取决于函数的调用方式
执行阶段:逐行执行代码,赋值变量、调用函数等。
销毁阶段:当函数执行完毕后,其执行上下文就会从调用栈中弹出并被销毁,同时该函数执行期间创建的所有局部变量和参数等也会随之被销毁(除非它们被闭包捕获)
注意:函数的定义(即函数体本身)并不包含在执行上下文中。
函数的定义是存储在内存中的一个固定部分,这个定义是持久的。
从内存管理的角度看,执行上下文的销毁意味着其占用的内存空间被释放(局部变量等占用的内存),而函数定义作为代码的一部分,存储在内存的代码区域,只要有引用指向函数定义,它就不会被垃圾回收机制回收。
因此,当函数的执行上下文被销毁时,只是表示该函数的一次特定执行结束了,相关的局部变量和参数等不再可用。但这并不影响函数定义的持久性,因为函数定义本身并不依赖于任何特定的执行上下文。实际上,函数定义可以被多次调用,每次调用都会创建一个新的执行上下文来执行函数体中的代码。
总结:函数的定义是持久的,存储在内存中,可以被多次调用,每次调用都会创建一个新的执行上下文。
函数的执行上下文是函数执行时所需的一个临时环境,它包含了函数执行期间的所有信息(参数、局部变量、this 等),并在函数执行完毕后被销毁。
但是这并不意味着函数执行期间创建的局部变量和参数会随之被释放,因为闭包所捕获的变量仍然会因为闭包的存在而在内存中保留,直到闭包不再被引用。
在 JavaScript 里,一个函数内部可以定义其他函数。如果内部函数使用了外部函数中的变量等,然后这个内部函数在外部函数执行完后,还能被外部访问和调用,此时就形成了闭包。闭包就像是一个 “包裹”,它把内部函数以及内部函数所依赖的外部函数的变量等环境都包起来了,即使外部函数执行结束,这些被包起来的变量等也不会被释放,因为内部函数用到了它们。
function A() { // 外部函数
let count = 0; // 外部函数中的变量
function B() { // 内部函数
count++; // 内部函数访问外部函数的变量
console.log(count)
};
return B
}
let a = A(); // 内部函数在外部函数之外被引用,闭包形成。a 是闭包函数 B 的引用。
a(); // 调用闭包函数,输出 1
a(); // 调用闭包函数,输出 2
/* 过程解释:
1. 当你调用 A() 时,函数 A 会被执行,执行过程中会创建一个局部变量 count,并且定义了函数 B。
2. 然后,A 会返回 B,返回的是函数 B 的引用。
3. 由于 B 是在 A 的作用域内定义的,所以 B 内部对 count 变量的引用依赖于 A 中的执行环境。
4. let a = A():将 B 赋值给变量 a,因此 a 现在指向函数 B,a 是一个引用。
5. 函数 A 执行完毕后,其执行上下文会被销毁。通常情况下,函数内部的局部变量会随着执行上下文的销毁而被释放,但这里存在特殊情况。
6. 调用 a():由于 a 实际上是函数 B,调用 a() 就相当于调用 B()。函数 B 内部引用了函数 A 中的变量 count,这就形成了一个闭包。
7. 虽然函数 A 的执行上下文被销毁了,但由于闭包的存在,函数 A 作用域中的变量 count 仍然保留在内存中。可以将其理解为函数 B 持有了对函数 A 作用域中变量的引用,只要函数 B 存在,这些变量就不能被垃圾回收机制回收。
*/
/* 所以:
引用 a 和变量 count 它们会一直保存在内存中,无法被垃圾回收机制回收。
只要没有显示删除它,就会一直在内存中。
*/
// 如果想释放 a 和 count 的内存,要加一句代码:
a = null;
/*
此时,变量 a 不再引用函数 B,即没有其他明确的引用指向闭包函数 B 了。
函数 B 由于没有了外部引用,就会被标记为可回收对象。
而变量 count 只有通过函数 B 这个闭包才能被访问到,当函数 B 成为可回收对象后,
count 也不再有有效的引用路径,同样会被标记为可回收对象。
在下一次垃圾回收执行时,它们就会被回收,释放所占用的内存。
*/
简单来说:函数 A 内部有一个函数 B,函数 B 引用了函数 A 中的变量,函数 B 在函数 A 外部被引用,那么函数 B 就是闭包。
为什么闭包要被 return ?
因为如果闭包没有被 return,那就不会创建一个可以被外部访问的闭包实例,也就是说,函数 A 内部的变量只能在函数内部使用,永远无法被外部读取。
闭包的作用:
可以在外部函数内部的变量
让这些变量的值始终保持在内存中
数据封装和私有变量
闭包的缺点:
从最开始的示例就可以知道,闭包的缺点就是容易导致内存泄漏,要注意显示地删除闭包地引用。
实际开发中哪些场景使用了闭包
数据封装和私有变量
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))
当函数作为普通函数被调用时(即不是作为对象的方法、不是构造函数、不是使用 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 用于在创建新对象时引用该对象本身。
function Person() {
this.name = 'a';
this.age = 10;
console.log("this:",this); // {name: 'a', age: 10}
}
let p = new Person()
箭头函数的 this 总是引用定义它的那个上下文(即包含它的函数或全局上下文)中的 this 值。
function Person() {
this.name = 'a';
this.age = 10;
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
},1000)
}
let p = new Person()
使用 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!'
这 3 者的作用:改变函数执行时内部 this
的指向。
为什么需要改变
this
的指向?
例如当一个函数 A 作为回调函数传递时,那么函数 A 中的 this 的指向就有可能不是你预期的,就比如说回调函数被 setTimeout、setInterval 等调用,那么这时候 this 的指向可能是全局对象或 undefined;
再例如下面这个例子:
function test(param1, param2) {
console.log('性别:' + this.sex + ';' + param1 + ';' + param2)
}
let man = {
sex: '男',
age: 10
}
let woman = {
sex: '女',
age: 10
}
有两个不同的对象都想调用 test 方法,该怎么做呢?你现在只能将这个函数放到对象中:
let man = {
sex: '男',
age: 10,
test: function(param1, param2) {
console.log('性别:' + this.sex + ';' + param1 + ';' + param2)
}
}
let 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 的指向,第二个参数传递有区别:
test.call(man, '游泳') // 性别:男;爱好1:游泳;爱好2:undefined
test.call(man, '游泳', '健身') // 性别:男;爱好1:游泳;爱好2:健身
// 超出的参数不处理
test.call(man, '游泳', '健身', '玩球') // 性别:男;爱好1:游泳;爱好2:健身
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, '游泳') // 报错,只能接收数组
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
回调函数内部执行的,调用者无需关心内部的具体细节。
优点:
解耦:回调函数允许我们将任务的执行与任务完成后的操作分离
灵活:可以根据需要传递不同的函数作为回调函数
缺点:
回调地狱:当多个异步操作需要顺序执行,并且每个操作都依赖于前一个操作的结果时,就会导致嵌套的回调函数层级过深,使得代码难以阅读和维护。
错误处理:在多层嵌套的回调函数中,错误处理会变得复杂
针对回调函数的缺点有哪些解决办法?
使用 Promise 或 async/await