第五阶段:函数体系-1
5.1 函数基础
- 函数声明:使用 function 关键字定义函数,语法为 function 函数名(参数列表) { 函数体 }。例如:
function add(a, b) {
return a + b;
}
- 函数提升:在 JavaScript 中,函数声明会被提升到其所在作用域的顶部,意味着在函数声明之前就可以调用该函数。例如:
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}
- 这是通过将匿名函数赋值给一个变量来定义函数的方式。例如:
const multiply = function (a, b) {
return a * b;
};
- 与函数声明不同,函数表达式不存在函数提升。如果在定义之前调用,会报错。例如:
console.log(multiply(2, 3)); // ReferenceError: multiply is not defined
const multiply = function (a, b) {
return a * b;
};
函数带()和不带()的区别
在 JavaScript 中,函数带 () 和不带 () 有着显著区别,主要体现在以下方面:
-
函数调用时机与返回值
带 ():表示立即调用函数。当 JavaScript 引擎解析到函数名后跟 () 时,会立即执行函数内的代码,并返回函数的执行结果(如果有返回值)。例如:
function add(a, b) {
return a + b;
}
let result1 = add(2, 3);
console.log(result1);
这里 add(2, 3) 就是立即调用 add 函数,将 2 和 3 作为参数传入,函数执行并返回 5,然后将返回值赋给 result1 并打印。
不带 ():此时函数名代表函数本身,即函数对象。它不会立即执行函数代码,而是可以将函数作为值进行传递、赋值给变量等操作。例如:
function greet() {
console.log('Hello!');
}
let greetFunction = greet;
greetFunction();
这里 greet 不带 (),将函数对象赋值给 greetFunction 变量,之后通过 greetFunction() 调用函数。
-
在事件绑定等场景中的应用
带 ():在事件绑定等场景中如果直接写函数带 (),函数会在代码解析到该位置时立即执行,而不是在事件触发时执行。例如:
<!DOCTYPE html>
<html>
<body>
<button id="btn">点击</button>
<script>
function clickHandler() {
console.log('按钮被点击了');
}
document.getElementById('btn').onclick = clickHandler();
</script>
</body>
</html>
这里 clickHandler() 会在页面加载解析到这行代码时就执行,而不是在按钮被点击时执行,并且由于 clickHandler 无返回值,onclick 实际上被赋值为 undefined,导致按钮点击无响应。
不带 ():正确的做法是在事件绑定中使用函数不带 (),这样函数会在事件触发时才执行。例如:
<!DOCTYPE html>
<html>
<body>
<button id="btn">点击</button>
<script>
function clickHandler() {
console.log('按钮被点击了');
}
document.getElementById('btn').onclick = clickHandler;
</script>
</body>
</html>
此时 clickHandler 不带 (),将函数对象赋值给 onclick 属性,当按钮被点击时,clickHandler 函数才会执行。
- 匿名函数:没有函数名的函数,通常用于作为参数传递给其他函数或在需要即时定义并使用函数的场景。例如:
setTimeout(function () {
console.log('这是一个匿名函数');
}, 1000);
- 自执行函数 IIFE(立即调用的函数表达式):定义后立即执行的匿名函数。语法为 (function () { /* 函数体 */ })(); 或 (function () { /* 函数体 */ }());。例如:
(function () {
let message = '自执行函数';
console.log(message);
})();
- 函数调用:通过函数名加括号的方式执行函数,如果有参数则在括号内传入。例如:
function greet(name) {
return `Hello, ${name}!`;
}
let greeting = greet('John');
console.log(greeting); // Hello, John!
- 参数:函数定义时括号内的变量为形参,调用函数时传入的值为实参。例如上面 greet 函数中,name 是形参,'John' 是实参。
- 返回值 return:使用 return 语句返回函数的执行结果。如果没有 return 语句,函数默认返回 undefined。
- 形参:函数定义时指定的参数,是接收实参的变量。例如 function sum(a, b) { /*… */ } 中,a 和 b 是形参。
- 实参:调用函数时实际传入的值。例如 sum(3, 5) 中,3 和 5 是实参。
- arguments 类数组对象:在函数内部,arguments 对象包含了调用函数时传入的所有实参。它不是真正的数组,而是伪数组,但有 length 属性且可以通过索引访问元素。例如:
function printArgs() {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
printArgs(1, 'hello', true);
- 如果函数有 return 语句,会返回指定的值。例如:
function getNumber() {
return 5;
}
let num = getNumber();
console.log(num); // 5
- 如果函数没有 return 语句,或者 return 后没有跟值,函数默认返回 undefined。例如:
function printMessage() {
console.log('这是一条消息');
}
let result = printMessage();
console.log(result); // undefined
- 函数作为参数:将一个函数作为另一个函数的参数传递(回调函数)。例如:
function add(a, b) {
return a + b;
}
function calculate(func, a, b) {
return func(a, b);
}
let result1 = calculate(add, 2, 3);
console.log(result1); // 5
- 函数作为返回值:一个函数可以返回另一个函数。例如:
function createAdder(x) {
return function (y) {
return x + y;
};
}
let addFive = createAdder(5);
let sum = addFive(3);
console.log(sum); // 8
5.2 ES6 + 函数新特性
简写规则:
- 单参数省略括号:当箭头函数只有一个参数时,可以省略参数的括号。例如:
// 普通写法
let func1 = function (num) {
return num * 2;
};
// 简写
let func2 = num => {
return num * 2;
};
- 单行省略 return 大括号:如果箭头函数的函数体只有一行代码,并且是返回值,可以省略 return 关键字和大括号。例如:
// 普通写法
let func3 = function (a, b) {
return a + b;
};
// 简写
let func4 = (a, b) => a + b;
无 this、无 arguments、不可 new、无原型:
- 无 this:箭头函数本身没有 this,它的 this 继承自外层作用域。例如:
let obj = {
name: 'John',
sayHello: function () {
setTimeout(() => {
console.log(`Hello, ${this.name}`);
}, 1000);
}
};
obj.sayHello(); // Hello, John
- 无 arguments:不能像普通函数那样使用 arguments 对象,可使用剩余参数替代。
- 不可 new:不能使用 new 关键字调用箭头函数,因为它没有自身的 this,也没有构造函数的内部方法。
- 无原型:箭头函数没有 prototype 属性,因为它不能用作构造函数。
- 可以在函数定义时为参数指定默认值,当调用函数没有传入该参数时,会使用默认值。例如:
function greet(name = 'Guest') {
console.log(`Hello, ${name}!`);
}
greet(); // Hello, Guest!
greet('John'); // Hello, John!
- 是什么:用于将函数的剩余参数收集到一个数组中。
- 为什么:相比于 arguments 对象,剩余参数是真正的数组,有数组的所有方法,使用更方便。
- 怎么用:
function sum(…nums) {
return nums.reduce((acc, num) => acc + num, 0);// 使用数组的reduce方法求和
}
let result = sum(1, 2, 3);
console.log(result); // 6
从对象或数组中提取值作为函数参数。
- 对象解构:例如:
function printUser({name, age}) {
console.log(`Name: ${name}, Age: ${age}`);
}
let user = {name: 'Jane', age: 25};
printUser(user); // Name: Jane, Age: 25
- 数组解构:例如:
function printCoords([x, y]) {
console.log(`X: ${x}, Y: ${y}`);
}
let coords = [10, 20];
printCoords(coords); // X: 10, Y: 20
- 在函数定义或调用时,最后一个参数后可以加逗号。例如:
function func(a, b, ) {
return a + b;
}
func(1, 2, );
这样在添加或删除参数时,方便代码维护,也使版本控制系统中的 diff 更清晰。
5.3 函数上下文 this
默认绑定:
- 是什么:在非严格模式下,普通独立函数调用时,this 指向全局对象,在浏览器中是 window,在 Node.js 中是 global。例如:
function sayHello() {
console.log(this);
}
sayHello(); // 在浏览器中输出 window 对象
-
为什么:这是 JavaScript 早期设计的一种默认行为,为了方便在全局作用域下使用 this 来访问全局对象的属性和方法。
-
怎么用:直接调用函数,此时函数内的 this 就遵循默认绑定规则。
隐式绑定:
- 是什么:当函数作为对象的方法被调用时,this 指向该对象。例如:
let person = {
name: 'John',
sayName: function () {
console.log(this.name);
}
};
person.sayName(); // 输出 John
- 为什么:这样设计使得对象的方法能够方便地访问对象自身的属性和方法,符合面向对象编程的逻辑。
- 怎么用:将函数定义为对象的属性(对象方法),通过对象调用该函数,函数内的 this 就指向该对象。
显式绑定:
-
是什么:使用 call、apply 和 bind 方法可以强制指定函数调用时 this 的指向。
-
call:第一个参数是 this 要指向的对象,后面可以跟多个参数。例如:
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
let person1 = {name: 'Jane'};
greet.call(person1, 'Hello'); // 输出 Hello, Jane
- apply:第一个参数也是 this 要指向的对象,第二个参数是一个数组,数组中的元素作为函数的参数。例如:
function sum(a, b) {
return a + b;
}
let numbers = [3, 5];
let result = sum.apply(null, numbers); // 输出 8
- bind:返回一个新函数,新函数内部的 this 被绑定到传入的第一个参数。例如:
function sayAge() {
console.log(this.age);
}
let person2 = {age: 25};
let boundSayAge = sayAge.bind(person2);
boundSayAge(); // 输出 25
-
为什么:在一些场景下,我们需要临时改变函数执行时 this 的指向,以复用函数逻辑。
-
怎么用:根据需要选择 call、apply 或 bind 方法,传入 this 指向的对象及其他参数。
new 绑定:
- 是什么:当使用 new 关键字调用构造函数时,会创建一个新的对象实例,构造函数内部的 this 指向这个新实例。例如:
function Person(name) {
this.name = name;
}
let newPerson = new Person('Bob');
console.log(newPerson.name); // 输出 Bob
- 为什么:这是 JavaScript 实现面向对象编程中实例化对象的一种机制,使得每个实例都有自己独立的属性。
- 怎么用:定义构造函数,使用 new 关键字调用构造函数来创建实例,构造函数内的 this 就指向新创建的实例。
箭头函数 this:
- 是什么:箭头函数本身没有自己的 this,它的 this 继承自外层作用域。例如:
let obj = {
name: 'Alice',
sayHello: function () {
setTimeout(() => {
console.log(`Hello, ${this.name}`);
}, 1000);
}
};
obj.sayHello(); // 输出 Hello, Alice
- 为什么:为了提供一种更简洁的函数定义方式,并且避免在嵌套函数中处理 this 指向的复杂性。
- 怎么用:在需要简洁的函数定义且希望 this 继承自外层作用域时使用箭头函数。
- 在浏览器环境中,全局作用域下的 this 指向 window 对象。例如:
console.log(this === window); // true
- 在 ES6 引入 globalThis 后,它提供了一种在各种环境(浏览器、Node.js 等)中都能访问全局对象的标准方式。例如:
console.log(globalThis === window); // 在浏览器中为 true
- 在严格模式下,独立函数调用时 this 不再指向全局对象,而是 undefined。例如:
function test() {
'use strict';
console.log(this);
}
test(); // 输出 undefined
- 这是为了避免意外的全局变量创建和更严格的 this 指向规则,提升代码的安全性和可维护性。
- 定时器:
let name = 'global';
let obj = {
name: 'obj',
printName: function () {
setTimeout(function () {
console.log(this.name); // 输出 global,因为这里的 this 是默认绑定,指向全局对象
}, 1000);
}
};
obj.printName();
// 使用箭头函数解决
obj.printName = function () {
setTimeout(() => {
console.log(this.name); // 输出 obj,箭头函数的 this 继承自外层的 obj
}, 1000);
};
obj.printName();
- DOM事件:
let button = document.getElementById('myButton');
button.onclick = function () {
console.log(this); // 输出 button 元素,因为 this 是隐式绑定到触发事件的元素
};
- 对象方法:
let animal = {
type: 'dog',
speak: function () {
console.log(`I'm a ${this.type}`);
}
};
animal.speak(); // 输出 I'm a dog,this 隐式绑定到 animal 对象
- 嵌套函数:
let outerObj = {
name: 'outer',
printInnerName: function () {
function innerFunction() {
console.log(this.name); // 输出 undefined,因为 innerFunction 是独立函数调用,this 默认绑定为 undefined
}
innerFunction();
}
};
outerObj.printInnerName();
// 使用箭头函数解决
outerObj.printInnerName = function () {
let innerArrowFunction = () => {
console.log(this.name); // 输出 outer,箭头函数的 this 继承自外层的 outerObj
};
innerArrowFunction();
};
outerObj.printInnerName();
5.4 作用域与闭包
- 全局作用域:在代码的最外层定义的变量和函数处于全局作用域。整个页面任意位置都能访问。例如:
// 全局变量
const globalStr = "我是全局";
function test() {
console.log(globalStr); // 内部可读取全局
}
test();
console.log(globalStr); // 外部直接读取
- 函数作用域:在函数内部定义的变量,其作用域仅限于该函数内部(局部变量)。例如:
function func() {
let localVar = 20;
console.log(localVar); // 20
}
func();
console.log(localVar); // localVar is not defined
- 块级作用域(let/const)「ES6+」:使用 let 或 const 声明的变量在块级作用域(如 if、for 块)内有效。例如:
if (true) {
let blockVar = 30;
}
console.log(blockVar); // blockVar is not defined
- 是什么:当在函数内部访问一个变量时,JavaScript 引擎首先在当前函数作用域中查找该变量,如果找不到,就会到上一级作用域查找,以此类推,直到全局作用域。这种由内向外的作用域嵌套形成的链条就是作用域链。
- 为什么:它确保了变量的有序访问,使得函数可以访问其外部作用域的变量,同时避免变量命名冲突。
- 怎么用:无需额外操作,在编写代码时,变量的查找会自动遵循作用域链规则。例如:
let outerVar = 10;
function outerFunc() {
let innerVar = 20;
function innerFunc() {
console.log(outerVar); // 10,在当前函数找不到 outerVar,到外层作用域找到
console.log(innerVar); // 20
}
innerFunc();
}
outerFunc();
- 是什么:闭包是指函数能够记住并访问其外部作用域的变量,即使在其外部作用域已经执行完毕。例如:
function outer() {
let num = 10;
function inner() {
return num;
}
return inner;
}
let closureFunc = outer();
console.log(closureFunc()); // 10,outer 执行完毕后,num 变量依然被 inner 函数引用,未被销毁
- 为什么:闭包的存在使得函数可以 “封装” 一部分数据,实现数据的隐藏和复用。
- 怎么用:通过函数嵌套,内部函数引用外部函数的变量,并将内部函数返回,从而形成闭包。
- 私有变量:利用闭包可以实现类似私有变量的效果。例如:
function Counter() {
let count = 0;
return {
increment: function () {
count++;
return count;
},
getCount: function () {
return count;
}
};
}
let counter = Counter();
console.log(counter.increment()); // 1
console.log(counter.getCount()); // 1
// count 无法在外部直接访问,实现了私有变量的效果
-
防抖节流:
-
防抖:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。例如:
function debounce(func, delay) {
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
function handleClick() {
console.log('Clicked');
}
let debouncedClick = debounce(handleClick, 500);
document.addEventListener('click', debouncedClick); -
节流:规定在一个单位时间内,只能触发一次函数。例如:
function throttle(func, delay) {
let lastTime = 0;
return function () {
let now = new Date().getTime();
if (now– lastTime >= delay) {
func.apply(this, arguments);
lastTime = now;
}
};
}
function handleScroll() {
console.log('Scrolled');
}
let throttledScroll = throttle(handleScroll, 1000);
window.addEventListener('scroll', throttledScroll);
-
-
缓存:闭包可以缓存函数的计算结果。例如:
function memoize() {
let cache = {};
return function (num) {
if (cache[num]) {
return cache[num];
}
let result = num * num;
cache[num] = result;
return result;
};
}
let memoizedCalculation = memoize();
console.log(memoizedCalculation(5)); // 25,第一次计算并缓存
console.log(memoizedCalculation(5)); // 25,直接从缓存获取
- 柯里化:将一个多参数函数转换为一系列单参数函数。例如:
function add(a) {
return function (b) {
return a + b;
};
}
let add5 = add(5);
console.log(add5(3)); // 8
- 是什么:如果闭包引用的变量长期不释放,会导致内存占用过高,造成内存泄漏。例如:
function outer() {
let largeData = new Array(1000000).fill(1);
function inner() {
return largeData.length;
}
return inner;
}
let closure = outer();
// 这里 closure 持续引用 largeData,即使 outer 执行完毕,largeData 也不会被垃圾回收,可能导致内存泄漏
- 为什么:因为闭包会阻止其引用的变量被垃圾回收机制回收,如果闭包的生命周期过长且引用了大量数据,就会导致内存问题。
- 怎么避免:尽量减少闭包中对不必要变量的引用,及时释放不再使用的闭包。例如在上述例子中,如果不再需要 closure,将其设置为 null,让垃圾回收机制可以回收相关内存。closure = null;
实战练习:
思考题:





