JavaScript系列02-函数深入理解
- IT业界
- 2025-09-19 11:12:02

本文介绍了JavaScript函数相关知识,包括
函数声明与函数表达式 - 解释两者的区别,提升行为,以及使用场景箭头函数特性 - 讲解语法、词法this、不能作为构造函数等特点this绑定机制 - 详细讲解四种绑定规则:默认绑定、隐式绑定、显式绑定和new绑定call、apply与bind - 解释这三个方法的使用方式与区别高阶函数与函数柯里化 - 从基础到实际应用函数式编程思想 - 介绍核心概念、纯函数、不可变性、函数组合等 1、函数声明与函数表达式在JavaScript中,创建函数有两种主要方式:函数声明和函数表达式。虽然它们看起来相似,但行为却有重要差异。
函数声明函数声明使用function关键字,后跟函数名:
function sayHello(name) { return "你好," + name; }特点:
会被完整提升到当前作用域的顶部在定义前可以调用必须有函数名 函数表达式函数表达式将函数赋值给变量:
const sayHello = function(name) { return "你好," + name; }; // 也可以命名(但名称只在函数内部可见) const sayGoodbye = function goodbye(name) { return "再见," + name; };特点:
变量声明会提升,但赋值不会必须先定义后使用函数名可选 两者区别函数声明在代码执行前就已完全可用,而函数表达式需要等到代码执行到那一行。
// 可以正常工作 hello("小明"); function hello(name) { console.log("你好," + name); } // 会报错 goodbye("小红"); // TypeError: goodbye is not a function const goodbye = function(name) { console.log("再见," + name); }; 2、箭头函数特性ES6引入的箭头函数不仅仅是函数的简写形式,它还有独特的行为特性。
基本语法 // 基本形式 const add = (a, b) => a + b; // 单个参数可以省略括号 const double = x => x * 2; // 无参数需要空括号 const sayHi = () => "你好"; // 函数体有多行语句需要大括号和return const calculate = (a, b) => { const result = a * b; return result + 5; }; 箭头函数的独特特性 没有自己的this:箭头函数不创建自己的this上下文,而是继承定义时的上下文没有arguments对象:不能使用arguments,但可以使用剩余参数(…args)不能作为构造函数:不能使用new操作符没有prototype属性不能用作Generator函数:不能使用yield关键字 箭头函数与普通函数this对比 const obj = { name: "对象", // 普通函数方法 regularMethod: function() { console.log(this.name); // "对象" setTimeout(function() { console.log(this.name); // undefined (在非严格模式下是窗口对象名) }, 100); }, // 箭头函数方法 arrowMethod: function() { console.log(this.name); // "对象" setTimeout(() => { console.log(this.name); // "对象",继承外部this }, 100); } }; 3、this绑定机制JavaScript中的this是许多困惑的来源。理解this的绑定规则,可以帮助我们精确预测代码行为。
this的本质在JavaScript中,this是一个特殊的关键字,它不同于普通变量,其值在函数执行时动态确定。简单来说,this指向"当前执行代码的上下文对象"。理解this机制对于掌握JavaScript至关重要。
核心原则:this的值不取决于函数在哪里定义,而取决于函数如何被调用。
this的四种绑定规则 默认绑定在非严格模式下,独立函数调用时,this指向全局对象(浏览器中的window,Node.js中的global)。在严格模式下,this为undefined。
function showThis() { console.log(this); } showThis(); // window或global (非严格模式) 隐式绑定当函数作为对象方法调用时,this指向该对象。
const user = { name: "张三", greet() { console.log(`你好,我是${this.name}`); } }; user.greet(); // 输出: 你好,我是张三注意:隐式绑定容易丢失,例如:
const greet = user.greet; greet(); // 输出: 你好,我是undefined (隐式绑定丢失) 显式绑定使用call、apply或bind方法可以明确指定函数执行时的this值。
function introduce(hobby1, hobby2) { console.log(`我是${this.name},我喜欢${hobby1}和${hobby2}`); } const person = { name: "李四" }; introduce.call(person, "读书", "游泳"); introduce.apply(person, ["音乐", "旅行"]); const boundFn = introduce.bind(person, "编程"); boundFn("跑步"); // 我是李四,我喜欢编程和跑步 new绑定使用new调用构造函数时,this指向新创建的对象实例。
function Person(name) { this.name = name; this.sayName = function() { console.log(`我的名字是${this.name}`); }; } const john = new Person("约翰"); john.sayName(); // 输出: 我的名字是约翰 this绑定优先级绑定规则的优先级从高到低为:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
特殊情况与边界案例 1. 箭头函数中的this箭头函数没有自己的this绑定,它继承外围作用域的this值。这使箭头函数特别适合需要保持this一致性的场景。
const obj = { name: '张三', // 传统函数方法 sayLater1: function() { setTimeout(function() { console.log(`传统方式: 你好,${this.name}`); }, 1000); }, // 箭头函数方法 sayLater2: function() { setTimeout(() => { console.log(`箭头函数: 你好,${this.name}`); }, 1000); } }; obj.sayLater1(); // 输出:传统方式: 你好,undefined (this指向window) obj.sayLater2(); // 输出:箭头函数: 你好,张三 (this指向obj)重要特性:
箭头函数的this在定义时确定,不是在执行时箭头函数不能用作构造函数(不能与new一起使用)call、apply和bind无法改变箭头函数的this指向 2. 回调函数中的this回调函数是this绑定容易出问题的地方,因为控制权暂时转移给了其他代码。
class Counter { constructor() { this.count = 0; this.button = document.createElement('button'); this.button.innerText = '点击增加'; document.body.appendChild(this.button); // 错误方式:this将丢失 // this.button.addEventListener('click', this.increment); // 正确方式1:使用bind this.button.addEventListener('click', this.increment.bind(this)); // 正确方式2:使用箭头函数 // this.button.addEventListener('click', () => this.increment()); } increment() { this.count++; console.log(this.count); } } const myCounter = new Counter(); 3. 严格模式下的this严格模式(‘use strict’)会影响默认绑定规则:
function normalMode() { console.log(this); // window或global } function strictMode() { 'use strict'; console.log(this); // undefined } normalMode(); strictMode(); 4. call/apply/bind传入null或undefined当call/apply/bind的第一个参数是null或undefined时:
function showThis() { console.log(this); } // 非严格模式 showThis.call(null); // window或global showThis.apply(undefined); // window或global // 严格模式 function strictShowThis() { 'use strict'; console.log(this); } strictShowThis.call(null); // null strictShowThis.apply(undefined); // undefined 实用技巧与最佳实践 在类方法中保持this一致性 class Component { constructor(element) { this.element = element; // 在构造函数中绑定方法 this.handleClick = this.handleClick.bind(this); // 添加事件监听 this.element.addEventListener('click', this.handleClick); } handleClick() { console.log('元素被点击了!', this.element); } } React组件中的thisReact类组件中常见的this绑定方式:
class Button extends React.Component { constructor(props) { super(props); this.state = { clicked: 0 }; // 方法1:在构造函数中绑定 this.handleClick1 = this.handleClick1.bind(this); } // 常规方法:需要绑定 handleClick1() { this.setState({ clicked: this.state.clicked + 1 }); } // 方法2:使用箭头函数作为类属性(公共类字段语法) handleClick2 = () => { this.setState({ clicked: this.state.clicked + 1 }); } render() { return ( <div> <button onClick={this.handleClick1}>按钮1</button> <button onClick={this.handleClick2}>按钮2</button> {/* 方法3:在回调中使用箭头函数(不推荐,每次渲染会创建新函数) */} <button onClick={() => this.setState({ clicked: this.state.clicked + 1 })}> 按钮3 </button> </div> ); } } 使用bind可以预设参数 function multiply(x, y) { return x * y; } // 创建一个永远将第一个参数设为2的新函数 const double = multiply.bind(null, 2); console.log(double(3)); // 6 console.log(double(4)); // 8 // 在事件处理中传递额外数据 function handleItem(id, event) { console.log(`处理项目${id}`); // event仍然可用 console.log(event.target); } // 将ID预绑定到处理函数 document.getElementById('item1').addEventListener('click', handleItem.bind(null, 1)); document.getElementById('item2').addEventListener('click', handleItem.bind(null, 2)); 常见错误与陷阱 方法提取并独立调用 const user = { name: '小明', getName() { return this.name; } }; // 正确用法 console.log(user.getName()); // 小明 // 错误:方法提取后独立调用 const getName = user.getName; console.log(getName()); // undefined,因为应用了默认绑定 // 正确:使用bind保持this指向 const boundGetName = user.getName.bind(user); console.log(boundGetName()); // 小明 嵌套函数中的this丢失 const user = { name: '小红', delayedGreet() { function greeting() { console.log(`你好,${this.name}`); } // this丢失,因为greeting是独立调用 setTimeout(greeting, 1000); // 输出:你好,undefined // 解决方案1:保存外部this引用 const self = this; function greeting2() { console.log(`你好,${self.name}`); } setTimeout(greeting2, 1000); // 输出:你好,小红 // 解决方案2:使用箭头函数 setTimeout(() => { console.log(`你好,${this.name}`); }, 1000); // 输出:你好,小红 // 解决方案3:使用bind setTimeout(greeting.bind(this), 1000); // 输出:你好,小红 } }; user.delayedGreet(); this总结JavaScript中的this绑定机制看似复杂,但遵循明确的规则:
检查函数是否通过new调用(new绑定)检查是否通过call/apply/bind调用(显式绑定)检查函数是否在对象上下文中调用(隐式绑定)如果都不是,则使用默认绑定(非严格模式下为全局对象,严格模式下为undefined)对于箭头函数,忽略以上规则,使用外围作用域的this掌握这些规则和常见陷阱,可以帮助你准确预测和控制JavaScript代码中的this行为,编写出更加可靠和可维护的代码。
4、call、apply与bind这三个方法都是JavaScript函数原型链上的内置方法,它们让我们能够精确控制函数执行时的this指向。虽然功能相似,但使用方式和适用场景各不相同。
基本语法对比 // call语法 function.call(thisArg, arg1, arg2, ...); // apply语法 function.apply(thisArg, [arg1, arg2, ...]); // bind语法 const boundFunction = function.bind(thisArg, arg1, arg2, ...); call方法call方法立即调用函数,第一个参数指定this,后续参数依次传入函数。
function greet(greeting) { console.log(`${greeting},我是${this.name}`); } const person = { name: "王五" }; greet.call(person, "早上好"); // 输出: 早上好,我是王五 apply方法apply方法也立即调用函数,不同之处在于第二个参数是一个数组,包含了所有传给函数的参数。
function introduce(hobby1, hobby2) { console.log(`我是${this.name},我喜欢${hobby1}和${hobby2}`); } const person = { name: "赵六" }; introduce.apply(person, ["画画", "唱歌"]); // 输出: 我是赵六,我喜欢画画和唱歌 bind方法bind方法不会立即调用函数,而是返回一个新函数,这个新函数的this值被永久绑定到第一个参数。
function sayHi() { console.log(`你好,${this.name}`); } const person = { name: "小明" }; const boundHi = sayHi.bind(person); boundHi(); // 输出: 你好,小明 // 即使改变调用上下文,this仍然绑定到person const obj = { name: "小红", sayHi: boundHi }; obj.sayHi(); // 输出: 你好,小明 (不是"小红") 根本区别 call和apply:立即执行函数bind:返回一个新函数,可以稍后执行 参数传递方式 call:参数以逗号分隔列表形式传入apply:参数以数组形式传入bind:预设参数,返回的新函数可以接收额外参数 实际应用与对比 基础方法调用对比 const person = { name: '张三', age: 30 }; function introduce(job, hobby) { console.log(`我叫${this.name},今年${this.age}岁,职业是${job},爱好是${hobby}。`); } // 使用call方法 introduce.call(person, '工程师', '编程'); // 输出: 我叫张三,今年30岁,职业是工程师,爱好是编程。 // 使用apply方法 introduce.apply(person, ['教师', '阅读']); // 输出: 我叫张三,今年30岁,职业是教师,爱好是阅读。 // 使用bind方法 const boundIntroduce = introduce.bind(person, '医生'); boundIntroduce('游泳'); // 输出: 我叫张三,今年30岁,职业是医生,爱好是游泳。 借用数组方法处理类数组对象DOM操作中经常遇到类数组对象(如NodeList、arguments),它们不能直接使用数组方法。
function printArguments() { // 错误方法:arguments.forEach(arg => console.log(arg)); // TypeError: arguments.forEach is not a function // 使用call借用数组方法 Array.prototype.forEach.call(arguments, arg => { console.log(arg); }); // 或使用apply Array.prototype.forEach.apply(arguments, [arg => { console.log(arg); }]); // 现代方法:转换为真正的数组 const args = Array.from(arguments); // 或使用展开运算符 // const args = [...arguments]; args.forEach(arg => console.log(arg)); } printArguments('红', '橙', '黄', '绿'); 继承与方法复用 // 父构造函数 function Animal(name, species) { this.name = name; this.species = species; } Animal.prototype.introduce = function() { console.log(`我是${this.name},是一只${this.species}`); }; // 子构造函数 function Cat(name, breed) { // 借用父构造函数初始化共有属性 Animal.call(this, name, '猫'); this.breed = breed; } // 设置原型链 Cat.prototype = Object.create(Animal.prototype); Cat.prototype.constructor = Cat; // 添加子类特有方法 Cat.prototype.meow = function() { console.log(`${this.name}:喵喵喵!`); }; const myCat = new Cat('咪咪', '英短'); myCat.introduce(); // 输出: 我是咪咪,是一只猫 myCat.meow(); // 输出: 咪咪:喵喵喵! 函数柯里化实现 function curry(fn) { // 获取原函数参数长度 const argsLength = fn.length; // 利用闭包和递归实现 function curried(...args) { // 当参数够了,就直接执行 if (args.length >= argsLength) { // 注意这里用apply保证this正确传递 return fn.apply(this, args); } // 参数不够,继续收集参数 return function(...moreArgs) { // 递归调用,同时保证this的正确传递 return curried.apply(this, [...args, ...moreArgs]); }; } return curried; } // 测试函数 function calculateVolume(length, width, height) { return length * width * height; } const curriedVolume = curry(calculateVolume); console.log(curriedVolume(2)(3)(4)); // 24 console.log(curriedVolume(2, 3)(4)); // 24 console.log(curriedVolume(2)(3, 4)); // 24 事件处理器中保持this指向组件或类方法作为事件处理器时,this经常丢失,使用bind可以解决。
class Slideshow { constructor() { this.currentSlide = 0; this.slides = document.querySelectorAll('.slide'); // 错误用法:this将指向按钮而非Slideshow实例 // document.querySelector('.next').addEventListener('click', this.nextSlide); // 正确用法:绑定this document.querySelector('.next').addEventListener('click', this.nextSlide.bind(this)); // 另一种方式:在构造函数中用bind预绑定 // this.nextSlide = this.nextSlide.bind(this); // document.querySelector('.next').addEventListener('click', this.nextSlide); } nextSlide() { this.currentSlide = (this.currentSlide + 1) % this.slides.length; this.updateSlides(); } updateSlides() { this.slides.forEach((slide, index) => { if (index === this.currentSlide) { slide.classList.add('active'); } else { slide.classList.remove('active'); } }); } } // 创建幻灯片实例 const mySlideshow = new Slideshow(); 使用apply查找数组最大/最小值 const numbers = [5, 6, 2, 3, 7, 1]; // 使用apply传递数组作为参数列表 const max = Math.max.apply(null, numbers); // 7 const min = Math.min.apply(null, numbers); // 1 // 现代方法:使用展开运算符 const maxSpread = Math.max(...numbers); // 7 const minSpread = Math.min(...numbers); // 1 函数借用与重用不同对象可以共享方法,而不需要在原型链上添加。
const computer = { name: 'MacBook Pro', describe: function(processor, memory) { console.log(`这台${this.name}配置了${processor}处理器和${memory}内存`); } }; const phone = { name: 'iPhone' }; // 借用computer对象的describe方法 computer.describe.call(phone, 'A15', '6GB'); // 输出: 这台iPhone配置了A15处理器和6GB内存 // 预绑定phone创建新函数 const describePhone = computer.describe.bind(phone); describePhone('A16', '8GB'); // 输出: 这台iPhone配置了A16处理器和8GB内存 使用场景总结 选择call的情况 明确知道参数数量且较少需要立即执行函数参数已经以独立变量形式存在 选择apply的情况 参数已经存在于数组中使用内置方法处理数组(如Math.max、Math.min)处理类数组对象 选择bind的情况 需要延迟执行函数需要部分应用函数参数(柯里化)事件处理器保持this指向创建多个预配置功能相似但行为不同的函数 注意事项 (1)箭头函数没有自己的this,使用call/apply/bind无法改变箭头函数内部的this指向 const arrowFn = () => { console.log(this); }; const obj = { name: '测试' }; arrowFn.call(obj); // 仍然输出原this(通常是window或global),而非obj (2)bind是永久性的,已绑定函数不能再次绑定 function greet() { console.log(`你好,${this.name}`); } const person1 = { name: '小明' }; const person2 = { name: '小红' }; const greetPerson1 = greet.bind(person1); const attemptRebind = greetPerson1.bind(person2); // 无效! attemptRebind(); // 输出: 你好,小明 (而非小红) (3)将null或undefined作为第一个参数时的行为 function showThis() { console.log(this); } // 非严格模式下,会默认绑定到全局对象 showThis.call(null); // window或global对象 showThis.apply(undefined); // window或global对象 // 严格模式下,this就是传入的值 function strictFn() { 'use strict'; console.log(this); } strictFn.call(null); // null strictFn.apply(undefined); // undefined 5、高阶函数与函数柯里化 高阶函数高阶函数是指满足以下条件之一的函数:
接受一个或多个函数作为参数返回一个函数JavaScript中的数组方法如map、filter、reduce等都是高阶函数。
// 高阶函数示例 function multiplyBy(factor) { // 返回一个新函数 return function(number) { return number * factor; }; } const double = multiplyBy(2); const triple = multiplyBy(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 // 数组的高阶函数 const numbers = [1, 2, 3, 4, 5]; // map:转换每个元素 const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10] // filter:过滤元素 const evens = numbers.filter(n => n % 2 === 0); // [2, 4] // reduce:累积结果 const sum = numbers.reduce((acc, n) => acc + n, 0); // 15 函数柯里化柯里化是将一个接受多个参数的函数转换为一系列只接受单个参数的函数的技术。
// 普通函数 function add(a, b, c) { return a + b + c; } // 柯里化版本 function curriedAdd(a) { return function(b) { return function(c) { return a + b + c; }; }; } console.log(add(1, 2, 3)); // 6 console.log(curriedAdd(1)(2)(3)); // 6 // ES6箭头函数简化 const arrowCurriedAdd = a => b => c => a + b + c; console.log(arrowCurriedAdd(1)(2)(3)); // 6 通用柯里化函数 function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args); } else { return function(...args2) { return curried.apply(this, args.concat(args2)); }; } }; } const curriedSum = curry(function(a, b, c) { return a + b + c; }); console.log(curriedSum(1)(2)(3)); // 6 console.log(curriedSum(1, 2)(3)); // 6 console.log(curriedSum(1)(2, 3)); // 6 console.log(curriedSum(1, 2, 3)); // 6 柯里化的实际应用 // 创建特定格式的日期字符串 const formatDate = curry(function(template, date) { const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); return template .replace('YYYY', year) .replace('MM', month) .replace('DD', day); }); const formatChinese = formatDate('YYYY年MM月DD日'); const formatDash = formatDate('YYYY-MM-DD'); console.log(formatChinese(new Date())); // 如:2023年09月15日 console.log(formatDash(new Date())); // 如:2023-09-15 // 过滤特定属性的对象 const filterByProperty = curry(function(propName, value, array) { return array.filter(item => item[propName] === value); }); const filterByType = filterByProperty('type'); const filterFruits = filterByType('fruit'); const items = [ { name: '苹果', type: 'fruit' }, { name: '西红柿', type: 'vegetable' }, { name: '香蕉', type: 'fruit' } ]; console.log(filterFruits(items)); // [{ name: '苹果', type: 'fruit' }, { name: '香蕉', type: 'fruit' }] 6、函数式编程思想函数式编程是一种编程范式,它强调将计算视为数学函数的求值,避免状态变化和可变数据。
函数式编程的核心概念 纯函数:给定相同输入总是返回相同输出,没有副作用不可变性:状态不可修改,产生新值而非修改原值函数组合:将多个简单函数组合成复杂功能声明式编程:描述"做什么"而非"怎么做"高阶函数:函数作为一等公民可作为参数和返回值 纯函数与副作用 // 纯函数 function add(a, b) { return a + b; } // 非纯函数(有副作用) let total = 0; function addToTotal(value) { total += value; // 修改外部状态 return total; } 不可变性示例 // 非函数式方式(可变) const numbers = [1, 2, 3]; numbers.push(4); // 修改原数组 // 函数式方式(不可变) const numbers = [1, 2, 3]; const newNumbers = [...numbers, 4]; // 创建新数组 函数组合函数组合是将多个函数组合成一个函数,其中一个函数的输出作为下一个函数的输入。
// 基础函数 const double = x => x * 2; const increment = x => x + 1; const square = x => x * x; // 手动组合 const manualCompose = x => square(increment(double(x))); // 通用组合函数 function compose(...fns) { return function(x) { return fns.reduceRight((value, fn) => fn(value), x); }; } const composedFn = compose(square, increment, double); console.log(composedFn(3)); // 49 (相当于 square(increment(double(3)))) 函数式编程实战以下是一个数据处理的函数式编程示例:
// 假设我们有一个产品数据数组 const products = [ { id: 1, name: "笔记本电脑", price: 5000, category: "电子产品" }, { id: 2, name: "鼠标", price: 50, category: "电子产品" }, { id: 3, name: "键盘", price: 150, category: "电子产品" }, { id: 4, name: "咖啡杯", price: 30, category: "日用品" }, { id: 5, name: "书架", price: 200, category: "家具" } ]; // 需求:获取所有电子产品的名称,并按价格从高到低排序 // 非函数式方式 function getElectronicProductNames(products) { const result = []; for (let i = 0; i < products.length; i++) { if (products[i].category === "电子产品") { result.push(products[i]); } } result.sort((a, b) => b.price - a.price); const names = []; for (let i = 0; i < result.length; i++) { names.push(result[i].name); } return names; } // 函数式方式 const getElectronicProductNames = products => products .filter(product => product.category === "电子产品") .sort((a, b) => b.price - a.price) .map(product => product.name); console.log(getElectronicProductNames(products)); // 输出: ["笔记本电脑", "键盘", "鼠标"] 函数式编程的优势 可读性:代码更加简洁、声明式,易于理解意图可维护性:纯函数易于测试,无副作用减少bug并发安全:不可变数据减少并发问题懒计算:可以优化性能,只在需要时计算可组合性:小型独立函数可重组构建复杂功能 // 测试的简易性 function add(a, b) { return a + b; } // 容易测试 test('add 1 + 2 equals 3', () => { expect(add(1, 2)).toBe(3); }); 总结JavaScript函数是语言中极其强大的特性,理解函数的声明、表达式方式、箭头函数的特性、this的绑定机制以及call/apply/bind的应用是成为JavaScript高手的必经之路。
高阶函数和柯里化等函数式编程技术允许我们以更简洁、更优雅的方式编写代码。函数式编程思想通过强调纯函数、不可变数据和函数组合,帮助我们编写更易维护、更健壮的应用程序。
深入理解这些概念,不仅能让我们写出更高质量的代码,也能更好地理解现代前端框架和库的设计原理。
JavaScript系列02-函数深入理解由讯客互联IT业界栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“JavaScript系列02-函数深入理解”