MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript函数与事件处理的结合

2023-01-142.0k 阅读

JavaScript函数基础

函数定义与调用

在JavaScript中,函数是一种可复用的代码块,用于执行特定的任务。函数定义有多种方式,最常见的是函数声明方式。例如:

function addNumbers(a, b) {
    return a + b;
}
let result = addNumbers(3, 5);
console.log(result); 

上述代码定义了一个名为addNumbers的函数,它接受两个参数ab,返回它们的和。通过addNumbers(3, 5)的调用方式,将3和5作为参数传递给函数,并将返回值赋给result变量,最后打印出结果8。

除了函数声明,还可以使用函数表达式来定义函数。例如:

let multiplyNumbers = function(a, b) {
    return a * b;
};
let product = multiplyNumbers(4, 6);
console.log(product); 

这里通过let multiplyNumbers = function(a, b) {... }的形式定义了一个匿名函数,并将其赋值给multiplyNumbers变量。后续通过multiplyNumbers(4, 6)进行调用。

函数的参数

  1. 默认参数:从ES6开始,JavaScript允许为函数参数设置默认值。例如:
function greet(name = 'Guest') {
    console.log(`Hello, ${name}!`);
}
greet(); 
greet('John'); 

greet函数中,name参数有一个默认值'Guest'。当不传递参数调用greet()时,会使用默认值;当传递参数greet('John')时,则使用传递的值。

  1. 剩余参数:剩余参数允许将不定数量的参数收集到一个数组中。例如:
function sumAll(...numbers) {
    let total = 0;
    for (let num of numbers) {
        total += num;
    }
    return total;
}
let sum = sumAll(1, 2, 3, 4, 5);
console.log(sum); 

sumAll函数中,...numbers表示剩余参数,它会将传递给函数的所有参数收集到numbers数组中,然后通过循环计算它们的总和。

函数作用域

  1. 全局作用域与局部作用域:在JavaScript中,变量的作用域决定了变量的可访问性。全局作用域中的变量在整个脚本中都可以访问,而函数内部定义的变量具有局部作用域,只能在函数内部访问。例如:
let globalVar = 'I am global';
function testScope() {
    let localVar = 'I am local';
    console.log(globalVar); 
    console.log(localVar); 
}
testScope();
console.log(globalVar); 
console.log(localVar); 

在上述代码中,globalVar是全局变量,在函数内外都可以访问。localVar是局部变量,只能在testScope函数内部访问。最后一行console.log(localVar);会报错,因为在全局作用域中无法访问局部变量。

  1. 块级作用域:ES6引入了letconst关键字,它们具有块级作用域。例如:
{
    let blockVar = 'I am in block';
    console.log(blockVar); 
}
console.log(blockVar); 

在这个代码块中,使用let定义的blockVar变量只在块级作用域内有效。块外访问blockVar会报错。

事件处理基础

事件类型

  1. 鼠标事件:常见的鼠标事件包括click(点击)、mouseover(鼠标悬停)、mouseout(鼠标离开)等。例如,当用户点击一个按钮时,可能希望触发某些操作。
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Mouse Event Example</title>
</head>

<body>
    <button id="myButton">Click Me</button>
    <script>
        let button = document.getElementById('myButton');
        button.onclick = function () {
            console.log('Button clicked!');
        };
    </script>
</body>

</html>

上述代码获取了页面中的按钮元素,并为其click事件绑定了一个匿名函数,当按钮被点击时,会在控制台打印出Button clicked!

  1. 键盘事件:像keydown(按键按下)、keyup(按键松开)等属于键盘事件。以下是一个简单的示例,监听用户在输入框中按下的键:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Keyboard Event Example</title>
</head>

<body>
    <input type="text" id="inputField">
    <script>
        let input = document.getElementById('inputField');
        input.onkeydown = function (event) {
            console.log(`Key ${event.key} was pressed.`);
        };
    </script>
</body>

</html>

当用户在输入框中按下任意键时,会在控制台打印出按下的键名。

  1. 表单事件submit(表单提交)、change(表单元素值改变)等是常见的表单事件。例如,当用户提交一个表单时,可能需要验证表单数据:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Form Event Example</title>
</head>

<body>
    <form id="myForm">
        <input type="text" id="nameInput" required>
        <input type="submit" value="Submit">
    </form>
    <script>
        let form = document.getElementById('myForm');
        form.onsubmit = function () {
            let nameInput = document.getElementById('nameInput');
            if (nameInput.value === '') {
                alert('Name field cannot be empty.');
                return false;
            }
            return true;
        };
    </script>
</body>

</html>

在这个表单中,当用户点击提交按钮时,会触发onsubmit事件处理函数。函数检查nameInput的值是否为空,如果为空则弹出提示并阻止表单提交,否则允许提交。

事件传播

  1. 捕获阶段:事件从文档的根节点开始,自上而下向目标元素传播。例如,有一个包含按钮的div元素:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Event Capturing Example</title>
</head>

<body>
    <div id="outerDiv">
        <button id="myButton">Click Me</button>
    </div>
    <script>
        let outerDiv = document.getElementById('outerDiv');
        let button = document.getElementById('myButton');
        outerDiv.addEventListener('click', function () {
            console.log('Outer div click (capturing)');
        }, true);
        button.addEventListener('click', function () {
            console.log('Button click (capturing)');
        }, true);
    </script>
</body>

</html>

在上述代码中,为outerDivbutton元素都添加了click事件监听器,并将第三个参数设置为true,表示在捕获阶段处理事件。当点击按钮时,会先打印Outer div click (capturing),然后打印Button click (capturing)

  1. 目标阶段:事件到达目标元素本身,这是事件真正发生的阶段。在上述示例中,当点击按钮时,目标阶段就是按钮元素接收点击事件。

  2. 冒泡阶段:事件从目标元素开始,自下而上向文档的根节点传播。如果将上述示例中的第三个参数设置为false(默认值),表示在冒泡阶段处理事件:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Event Bubbling Example</title>
</head>

<body>
    <div id="outerDiv">
        <button id="myButton">Click Me</button>
    </div>
    <script>
        let outerDiv = document.getElementById('outerDiv');
        let button = document.getElementById('myButton');
        outerDiv.addEventListener('click', function () {
            console.log('Outer div click (bubbling)');
        }, false);
        button.addEventListener('click', function () {
            console.log('Button click (bubbling)');
        }, false);
    </script>
</body>

</html>

当点击按钮时,会先打印Button click (bubbling),然后打印Outer div click (bubbling)

JavaScript函数与事件处理的结合

内联事件处理与函数调用

  1. 内联方式绑定函数:在HTML标签中,可以直接通过on开头的属性来绑定事件处理函数。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Inline Event Handler Example</title>
</head>

<body>
    <button onclick="showMessage()">Click Me</button>
    <script>
        function showMessage() {
            alert('Hello from function!');
        }
    </script>
</body>

</html>

在这个例子中,按钮的onclick属性直接调用了showMessage函数。当按钮被点击时,会弹出一个包含Hello from function!的提示框。

  1. 传递参数:内联方式也可以向函数传递参数。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Inline Event with Parameter Example</title>
</head>

<body>
    <button onclick="showMessage('John')">Click Me</button>
    <script>
        function showMessage(name) {
            alert(`Hello, ${name}!`);
        }
    </script>
</body>

</html>

这里按钮点击时传递了'John'作为参数给showMessage函数,函数会根据传递的参数弹出相应的提示。

使用addEventListener绑定函数

  1. 基本绑定addEventListener方法提供了一种更灵活的方式来绑定事件处理函数。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>addEventListener Example</title>
</head>

<body>
    <button id="myButton">Click Me</button>
    <script>
        let button = document.getElementById('myButton');
        function handleClick() {
            console.log('Button clicked via addEventListener');
        }
        button.addEventListener('click', handleClick);
    </script>
</body>

</html>

在上述代码中,先获取按钮元素,定义了handleClick函数,然后通过addEventListenerhandleClick函数绑定到按钮的click事件上。

  1. 使用匿名函数:也可以直接在addEventListener中使用匿名函数。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>addEventListener with Anonymous Function Example</title>
</head>

<body>
    <button id="myButton">Click Me</button>
    <script>
        let button = document.getElementById('myButton');
        button.addEventListener('click', function () {
            console.log('Button clicked via anonymous function');
        });
    </script>
</body>

</html>

这种方式在处理简单事件逻辑时很方便,不需要单独定义一个命名函数。

  1. 传递参数:如果需要向事件处理函数传递参数,可以使用闭包的方式。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>addEventListener with Parameter Example</title>
</head>

<body>
    <button id="button1">Button 1</button>
    <button id="button2">Button 2</button>
    <script>
        function handleButtonClick(name) {
            return function () {
                console.log(`Button ${name} clicked`);
            };
        }
        let button1 = document.getElementById('button1');
        let button2 = document.getElementById('button2');
        button1.addEventListener('click', handleButtonClick('1'));
        button2.addEventListener('click', handleButtonClick('2'));
    </script>
</body>

</html>

在这个例子中,handleButtonClick函数返回一个内部函数,通过闭包的机制,内部函数可以访问到handleButtonClick函数的参数name。这样不同的按钮点击时会打印出不同的信息。

事件委托

  1. 原理:事件委托是利用事件冒泡的特性,将子元素的事件委托给父元素来处理。例如,有一个包含多个列表项的无序列表:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Event Delegation Example</title>
</head>

<body>
    <ul id="myList">
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
    </ul>
    <script>
        let list = document.getElementById('myList');
        list.addEventListener('click', function (event) {
            if (event.target.tagName === 'LI') {
                console.log(`Clicked on ${event.target.textContent}`);
            }
        });
    </script>
</body>

</html>

在上述代码中,为ul元素添加了click事件监听器。当点击任何一个li元素时,由于事件冒泡,ul元素会接收到点击事件。通过检查event.targettagName,可以确定是哪个li元素被点击,并打印出其文本内容。

  1. 优势:事件委托可以减少事件绑定的数量,提高性能。特别是当有大量子元素时,如果为每个子元素都绑定事件,会占用较多的内存和资源。通过事件委托,只需要在父元素上绑定一个事件处理函数即可。另外,动态添加的子元素也会自动受到事件委托的处理,不需要为新添加的元素重新绑定事件。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Dynamic Event Delegation Example</title>
</head>

<body>
    <ul id="myList">
        <li>Item 1</li>
        <li>Item 2</li>
    </ul>
    <button id="addButton">Add Item</button>
    <script>
        let list = document.getElementById('myList');
        let addButton = document.getElementById('addButton');
        list.addEventListener('click', function (event) {
            if (event.target.tagName === 'LI') {
                console.log(`Clicked on ${event.target.textContent}`);
            }
        });
        addButton.addEventListener('click', function () {
            let newLi = document.createElement('li');
            newLi.textContent = 'New Item';
            list.appendChild(newLi);
        });
    </script>
</body>

</html>

在这个例子中,点击“Add Item”按钮会动态添加一个新的列表项。由于事件委托的存在,新添加的列表项点击事件也能被正确处理。

函数作为事件处理程序的注意事项

  1. 作用域问题:在事件处理函数中,this的指向可能会与预期不同。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Scope in Event Handler Example</title>
</head>

<body>
    <button id="myButton">Click Me</button>
    <script>
        let myObject = {
            message: 'Hello from object',
            handleClick: function () {
                console.log(this.message);
            }
        };
        let button = document.getElementById('myButton');
        button.addEventListener('click', myObject.handleClick);
    </script>
</body>

</html>

在上述代码中,预期点击按钮时会打印出Hello from object,但实际上会打印undefined。这是因为在事件处理函数中,this指向的是触发事件的DOM元素(即按钮),而不是myObject。为了解决这个问题,可以使用bind方法:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Scope Fixed in Event Handler Example</title>
</head>

<body>
    <button id="myButton">Click Me</button>
    <script>
        let myObject = {
            message: 'Hello from object',
            handleClick: function () {
                console.log(this.message);
            }
        };
        let button = document.getElementById('myButton');
        button.addEventListener('click', myObject.handleClick.bind(myObject));
    </script>
</body>

</html>

通过myObject.handleClick.bind(myObject),将handleClick函数的this绑定到myObject,这样点击按钮时就能正确打印出Hello from object

  1. 内存泄漏:如果事件处理函数引用了外部的大对象,并且没有及时解除事件绑定,可能会导致内存泄漏。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Memory Leak Example</title>
</head>

<body>
    <div id="myDiv"></div>
    <script>
        let largeObject = {
            data: new Array(1000000).fill('a lot of data')
        };
        let div = document.getElementById('myDiv');
        div.addEventListener('click', function () {
            console.log(largeObject.data);
        });
        // 假设这里移除了div元素,但事件处理函数仍然引用着largeObject
        document.body.removeChild(div);
    </script>
</body>

</html>

在上述代码中,虽然移除了div元素,但由于事件处理函数仍然引用着largeObjectlargeObject不会被垃圾回收机制回收,从而导致内存泄漏。为了避免这种情况,在移除元素前应该先解除事件绑定:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>No Memory Leak Example</title>
</head>

<body>
    <div id="myDiv"></div>
    <script>
        let largeObject = {
            data: new Array(1000000).fill('a lot of data')
        };
        let div = document.getElementById('myDiv');
        function handleClick() {
            console.log(largeObject.data);
        }
        div.addEventListener('click', handleClick);
        // 移除元素前解除事件绑定
        div.removeEventListener('click', handleClick);
        document.body.removeChild(div);
    </script>
</body>

</html>

这样在移除div元素前,先通过div.removeEventListener('click', handleClick)解除了事件绑定,largeObject就可以被正常回收,避免了内存泄漏。

  1. 性能优化:在频繁触发的事件(如scrollresize等)中,应该避免执行复杂的操作。可以使用防抖(Debounce)或节流(Throttle)技术来优化性能。
    • 防抖:防抖是指在事件触发一定时间后才执行函数,如果在这段时间内再次触发事件,则重新计时。例如,对于窗口resize事件,可以这样实现防抖:
function debounce(func, delay) {
    let timer;
    return function () {
        let context = this;
        let args = arguments;
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(context, args);
        }, delay);
    };
}
window.addEventListener('resize', debounce(function () {
    console.log('Window resized (debounced)');
}, 300));

在上述代码中,debounce函数返回一个新的函数,这个新函数在事件触发时会清除之前的定时器,并重新设置一个定时器,延迟delay时间后执行真正的处理函数func。这样在窗口快速缩放时,console.log('Window resized (debounced)');不会频繁执行,只有在停止缩放300毫秒后才会执行一次。 - 节流:节流是指在一定时间内,只允许事件处理函数执行一次。例如,对于scroll事件,可以这样实现节流:

function throttle(func, interval) {
    let lastTime = 0;
    return function () {
        let context = this;
        let args = arguments;
        let now = new Date().getTime();
        if (now - lastTime >= interval) {
            func.apply(context, args);
            lastTime = now;
        }
    };
}
window.addEventListener('scroll', throttle(function () {
    console.log('Window scrolled (throttled)');
}, 200));

在这个throttle函数中,通过记录上次执行时间lastTime,当距离上次执行时间超过interval时,才允许再次执行处理函数func。这样在窗口滚动时,console.log('Window scrolled (throttled)');最多每200毫秒执行一次,避免了频繁执行复杂操作对性能的影响。

通过合理地结合JavaScript函数与事件处理,开发者可以创建出交互性强、性能良好的Web应用程序。无论是简单的按钮点击响应,还是复杂的页面交互逻辑,都可以通过这些技术来实现。同时,注意处理好作用域、内存泄漏和性能等问题,能使代码更加健壮和高效。