Kỹ thuật nâng cao trong JavaScript

Mục lục

  1. Sao chép nông/nhẹ và sâu 1.1 Sao chép nông 1.2 Sao chép sâu 1.3 Triển khai sao chép sâu bằng đệ quy

  2. Xử lý ngoại lệ 2.1 Gửi ngoại lệ bằng throw 2.2 Bắt lỗi bằng try/catch 2.3 Sử dụng debugger

  3. Xử lý this 3.1 Hiểu về this 3.2 Thay đổi this 3.2.1 call() - Cơ bản 3.2.2 apply() - Trung cấp 3.2.3 bind() - Nâng cao 3.2.4 So sánh call, apply, bind

  4. Tối ưu hiệu năng 4.1 Chống rung (debounce) 4.2 Hạn chế tần suất (throttle)

  5. Ví dụ tổng hợp sử dụng throttle


1. Sao chép nông/nhẹ và sâu

Sao chép nông và sâu chỉ áp dụng cho kiểu tham chiếu Sao chép nông: Sao chép địa chỉ tham chiếu Các phương pháp phổ biến:

  1. Sao chép đối tượng: Object.assign() / {...obj}
  2. Sao chép mảng: Array.prototype.concat() / [...arr] Nếu là kiểu dữ liệu đơn giản thì sao chép giá trị, kiểu tham chiếu thì sao chép địa chỉ (Hiểu đơn giản: Nếu là đối tượng đơn tầng không vấn đề, nếu có nhiều tầng sẽ gặp lỗi)
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    const data = {
      name: 'John',
      age: 30,
      address: {
        city: 'Hanoi'
      }
    }
    // Sao chép nông
    const clone = { ...data }
    clone.age = 35
    clone.address.city = 'Da Nang'
    console.log(clone)
    console.log(data)
  </script>
</body>
</html>

Sao chép sâu: Sao chép nội dung đối tượng, không phải địa chỉ Các phương pháp phổ biến:

  1. Triển khai sao chép sâu bằng đệ quy
  2. Sử dụng lodash.cloneDeep
  3. Sử dụng JSON.stringify()

1.3 Triển khai sao chép sâu bằng đệ quy

Hàm đệ quy: Hàm gọi chính nó trong thân hàm

  • Hiểu đơn giản: Hàm gọi chính nó, đây là hàm đệ quy
  • Tác dụng tương tự như vòng lặp
  • Do dễ xảy ra lỗi tràn ngăn xếp (stack overflow), cần thêm điều kiện dừng return
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    let count = 1
    function repeat() {
      console.log(`Lần thứ ${count}`)
      if (count >= 6) return
      count++
      repeat()
    }
    repeat()
  </script>
</body>
</html>

Bài tập: Sử dụng đệ quy để mô phỏng setInterval bằng setTimeout

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="time"></div>
  <script>
    function showTime() {
      document.getElementById('time').textContent = new Date().toLocaleString()
      setTimeout(showTime, 1000)
    }
    showTime()
  </script>
</body>
</html>

Phương pháp 1: Triển khai sao chép sâu bằng đệ quy (phiên bản đơn giản)

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    const source = {
      name: 'Alice',
      age: 25,
      hobbies: ['reading', 'coding'],
      contact: {
        phone: '123456789'
      }
    }
    const target = {}
    function deepCopy(dest, src) {
      for (let key in src) {
        if (Array.isArray(src[key])) {
          dest[key] = []
          deepCopy(dest[key], src[key])
        } else if (src[key] instanceof Object) {
          dest[key] = {}
          deepCopy(dest[key], src[key])
        } else {
          dest[key] = src[key]
        }
      }
    }
    deepCopy(target, source)
    console.log(target)
    target.hobbies[0] = 'gaming'
    target.contact.phone = '987654321'
    console.log(source)
  </script>
</body>
</html>

Phương pháp 2: Sử dụng lodash.cloneDeep

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="lodash.min.js"></script>
  <script>
    const source = {
      name: 'Bob',
      age: 40,
      skills: ['JavaScript', 'React'],
      profile: {
        role: 'Developer'
      }
    }
    const clone = _.cloneDeep(source)
    clone.profile.role = 'Manager'
    console.log(source)
  </script>
</body>
</html>

Phương pháp 3: Sử dụng JSON.stringify()

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    const source = {
      name: 'Charlie',
      age: 35,
      interests: ['music', 'sports'],
      bio: {
        status: 'active'
      }
    }
    const clone = JSON.parse(JSON.stringify(source))
    clone.bio.status = 'inactive'
    console.log(source)
  </script>
</body>
</html>

2. Xử lý ngoại lệ

Xử lý ngoại lệ giúp dự đoán lỗi có thể xảy ra trong quá trình thực thi và tối đa hóa việc tránh lỗi khiến chương trình dừng lại

2.1 Gửi ngoại lệ bằng throw

Tóm tắt:

  1. throw gửi thông tin lỗi, chương trình sẽ dừng thực thi
  2. throw sau đó là thông báo lỗi
  3. Sử dụng đối tượng Error cùng throw để thiết lập thông tin lỗi chi tiết hơn
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    function add(a, b) {
      if (!a || !b) {
        throw new Error('Thiếu tham số')
      }
      return a + b
    }
    console.log(add())
  </script>
</body>
</html>

2.2 Bắt lỗi bằng try/catch

Sử dụng try/catch để bắt lỗi (thông tin lỗi do trình duyệt cung cấp). try thử, catch bắt lỗi, finally luôn thực thi Tóm tắt:

  1. try...catch dùng để bắt lỗi
  2. Đặt đoạn mã có thể xảy ra lỗi vào try
  3. Nếu có lỗi trong try, sẽ chạy catch và bắt lỗi
  4. finally luôn được thực thi bất kể có lỗi hay không
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <p id="content">Nội dung</p>
  <script>
    function process() {
      try {
        const element = document.querySelector('.content')
        element.style.color = 'red'
      } catch (error) {
        console.log(error.message)
        throw new Error('Lỗi selector')
      } finally {
        alert('Thông báo')
      }
      console.log('Tiếp tục thực thi')
    }
    process()
  </script>
</body>
</html>

2.3 debugger

Sử dụng debugger để bắt lỗi (thông tin lỗi do trình duyệt cung cấp)


3. Xử lý this

this là một trong những đặc điểm hấp dẫn nhất của JavaScript, giá trị this có thể thay đổi tùy ngữ cảnh Mục tiêu: Nắm được giá trị mặc định của this trong các trường hợp khác nhau, biết cách thiết lập this động

  • this trong hàm thường phụ thuộc vào cách gọi, nếu không có người gọi thì this là window (trong chế độ nghiêm ngặt là undefined)
  • Mục tiêu: Nắm được this trong hàm mũi tên
  • Hàm mũi tên không có this riêng, this kế thừa từ phạm vi bên ngoài
  • Hàm mũi tên sẽ lấy this từ phạm vi gần nhất
  • Nếu không tìm thấy this trong phạm vi bên ngoài, sẽ tìm tiếp lên cấp trên
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button>Click me</button>
  <script>
    console.log(this) // window
    function show() {
      console.log(this) // window
    }
    window.show()
    window.setTimeout(function () {
      console.log(this) // window
    }, 1000)
    document.querySelector('button').addEventListener('click', function () {
      console.log(this) // button
    })
    const person = {
      greet: function () {
        console.log(this) // person
      }
    }
    person.greet()
  </script>
</body>
</html>

Lưu ý:

  • Trong các hàm callback DOM, không nên dùng hàm mũi tên nếu cần this tham chiếu đến phần tử DOM
  • Không nên dùng hàm mũi tên trong lập trình hướng đối tượng dựa trên prototype

Tóm tắt:

  1. Hàm mũi tên không có this riêng, kế thừa từ phạm vi bên ngoài
  2. Không phù hợp: Hàm constructor, hàm prototype, callback DOM
  3. Phù hợp: Khi cần truy cập this từ phạm vi bên ngoài

3.2 Thay đổi this

JavaScript cho phép chỉ định this trong hàm bằng 3 phương thức:

  • call()
  • apply()
  • bind()
3.2.1 call() - Cơ bản

Sử dụng call để gọi hàm và chỉ định this trong hàm

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    const user = {
      name: 'Eve'
    }
    function greet(x, y) {
      console.log(this.name, x + y)
    }
    greet.call(user, 1, 2)
  </script>
</body>
</html>
3.2.2 apply() - Trung cấp

Sử dụng apply để gọi hàm và chỉ định this trong hàm

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    const user = {
      age: 28
    }
    function greet(x, y) {
      console.log(this.age, x + y)
    }
    greet.apply(user, [3, 4])
    const numbers = [10, 20, 30]
    const max = Math.max.apply(null, numbers)
    const min = Math.min.apply(null, numbers)
    console.log(max, min)
  </script>
</body>
</html>
3.2.3 bind() - Nâng cao

bind() không gọi hàm nhưng thay đổi this trong hàm

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button>Send</button>
  <script>
    const user = {
      status: 'active'
    }
    function setStatus() {
      console.log(this.status)
    }
    const bound = setStatus.bind(user)
    bound()
    document.querySelector('button').addEventListener('click', function () {
      this.disabled = true
      setTimeout(() => {
        this.disabled = false
      }.bind(this), 2000)
    })
  </script>
</body>
</html>
3.2.4 So sánh call, apply, bind

Tương đồng: Đều thay đổi this trong hàm Khác biệt:

  • call và apply gọi hàm và thay đổi this
  • call truyền tham số dạng x, y..., apply truyền tham số dạng mảng
  • bind không gọi hàm, chỉ thay đổi this Ứng dụng:
  • call: Gọi hàm và truyền tham số
  • apply: Thường dùng với mảng, ví dụ tìm max/min trong mảng
  • bind: Thay đổi this mà không gọi hàm, ví dụ thay đổi this trong setTimeout

4. Tối ưu hiệu năng

4.1 Chống rung (debounce)

Chống rung: Sau khi kích hoạt sự kiện, hàm chỉ được thực thi một lần trong n giây. Nếu kích hoạt lại trong n giây, thời gian thực thi được tính lại Ví dụ: Chính sách mua nhà ở Bắc Kinh yêu cầu đóng bảo hiểm xã hội liên tục 5 năm. Nếu năm nào bảo hiểm xã hội bị gián đoạn, phải bắt đầu lại từ đầu Ứng dụng: Chống rung trong ô tìm kiếm

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
  <style>
    .box {
      width: 500px;
      height: 500px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>
<body>
  <div class="box"></div>
  <script>
    const box = document.querySelector('.box')
    let counter = 1
    function updateCounter() {
      box.textContent = ++counter
    }
    function debounce(fn, delay) {
      let timer
      return function () {
        if (timer) clearTimeout(timer)
        timer = setTimeout(fn, delay)
      }
    }
    box.addEventListener('mousemove', debounce(updateCounter, 200))
  </script>
</body>
</html>

4.2 Hạn chế tần suất (throttle)

Hạn chế tần suất: Kích hoạt sự kiện liên tục nhưng chỉ thực thi hàm một lần trong n giây Ứng dụng: Hiệu ứng cuộn trang, điều khiển slide, thay đổi kích thước cửa sổ

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
  <style>
    .box {
      width: 500px;
      height: 500px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>
<body>
  <div class="box"></div>
  <script>
    const box = document.querySelector('.box')
    let counter = 1
    function updateCounter() {
      box.textContent = ++counter
    }
    function throttle(fn, delay) {
      let lastCall = 0
      return function () {
        const now = Date.now()
        if (now - lastCall >= delay) {
          fn()
          lastCall = now
        }
      }
    }
    box.addEventListener('mousemove', throttle(updateCounter, 500))
  </script>
</body>
</html>

5. Ví dụ tổng hợp sử dụng throttle

Ghi lại vị trí phát video khi đóng trang

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Trường hợp tổng hợp</title>
  <style>
    * {
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }
    .container {
      width: 1200px;
      margin: 0 auto;
    }
    .video video {
      width: 100%;
      padding: 20px 0;
    }
    .elevator {
      position: fixed;
      top: 280px;
      right: 20px;
      z-index: 999;
      background: #fff;
      border: 1px solid #e4e4e4;
      width: 60px;
    }
    .elevator a {
      display: block;
      padding: 10px;
      text-decoration: none;
      text-align: center;
      color: #999;
    }
    .elevator a.active {
      color: #1286ff;
    }
    .outline {
      padding-bottom: 300px;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <a href="http://pip.itcast.cn">
        <img src="https://pip.itcast.cn/img/logo_v3.29b9ba72.png" alt="" />
      </a>
    </div>
    <div class="video">
      <video src="https://v.itheima.net/LapADhV6.mp4" controls></video>
    </div>
    <div class="elevator">
      <a href="javascript:;" data-ref="video">Video</a>
      <a href="javascript:;" data-ref="intro">Giới thiệu</a>
      <a href="javascript:;" data-ref="outline">Bình luận</a>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  <script>
    const video = document.querySelector('video')
    video.ontimeupdate = _.throttle(() => {
      localStorage.setItem('currentTime', video.currentTime)
    }, 1000)
    video.onloadeddata = () => {
      video.currentTime = localStorage.getItem('currentTime') || 0
    }
  </script>
</body>
</html>

Thẻ: JavaScript Sao chép sâu xử lý ngoại lệ this Debounce

Đăng vào ngày 20 tháng 6 lúc 04:02