JavaScript 基础知识复习(一)

0 阅读3分钟

一、instanceof 运算符详解

1.1 基本概念

instanceof 用于检测构造函数的原型对象是否出现在某个实例对象的原型链上。

1.2 工作原理

通过检查实例对象的原型链上是否包含构造函数的原型对象:

  • 如果包含,则实例对象是构造函数的一个实例,返回 true
  • 如果不包含,则返回 false

1.3 注意事项

  • 只能用于检查对象类型,不能用于基本类型的检查
  • 对基本类型使用 instanceof 会始终返回 false

1.4 手动实现

// 手写 instanceof 实现
function myInstanceof(obj, constructor) {
  // 检查 obj 必须不是基本类型
  // null 没有包装对象,直接判断就行
  if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
      return false;
  }
  
  // 检查 constructor 必须是函数
  if (typeof constructor !== 'function') {
    throw new TypeError('Right-hand side of \'instanceof\' is not callable');
  }
  
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === constructor.prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

1.5 局限性

基于原型链的检查机制,如果对象的原型链比较深,检查效率会相对较低。


二、原型链

2.1 基本概念

原型链是 JavaScript 实现继承的一种核心机制。

2.2 工作原理

  • 每个对象都有一个私有属性 __proto__
  • 每个构造函数都有一个 prototype 属性
  • 对象的 __proto__ 指向它的构造函数的 prototype
  • 构造函数的 prototype 也有自己的 __proto__

2.3 属性查找机制

当在对象中查找某个属性或方法时:

  1. 首先查找对象本身是否存在该属性或方法
  2. 如果不存在,JavaScript 引擎会去查找它的 __proto__(即构造函数的 prototype
  3. 如果仍不存在,继续查找构造函数的 prototype__proto__
  4. 以此类推,直到找到 Object.prototype.__proto__(即 null),到达原型链的尽头

2.4 核心作用

实现了 JavaScript 中的继承。 当一个对象需要继承另一个对象的属性和方法时,可以将父对象设置为子对象的原型对象,从而子对象能够通过原型链访问父对象的属性和方法。

// 原型继承示例 - 基于对象
const parent = {
  name: 'Parent',
  greet() {
    console.log(`Hello, I am ${this.name}`);
  }
};
// 创建子对象,原型指向 parent
const child = Object.create(parent);
console.log(child.__proto__ === parent); // true

// 原型继承示例 - 基于类
class Parent {
  constructor(name) {
    this.name = name;
  }
  greet() {
    console.log(`Hello, I am ${this.name}`);
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类构造函数,继承属性
    this.age = age;
  }
  sayAge() {
    console.log(`I am ${this.age} years old`);
  }
}

2.5 局限性

在原型链上查找非自身的属性比较耗时,对性能有一定影响,但现代引擎对此有高度优化,在一般情况下影响很小。


三、new 运算符原理

3.1 基本概念

new 运算符用于创建:

  • 具有构造函数的内置对象的实例
  • 用户定义的对象类型的实例

3.2 核心作用

通过构造函数创建一个新的对象实例,并将该对象绑定到构造函数的 this 上。

3.3 执行流程

  1. 创建空对象:生成一个新的空对象,该对象的原型指向构造函数的原型对象
  2. 绑定 this:将构造函数的 this 关键字绑定到刚创建的空对象上
  3. 执行构造函数:调用构造函数,执行其中的代码,通常用于初始化对象属性
  4. 返回结果
    • 如果构造函数显式返回一个对象(或函数),则返回该对象
    • 否则,返回步骤1创建的空对象

3.4 手动实现

// 手写 new 实现
function myNew(constructor, ...args) {
  const obj = Object.create(constructor.prototype);
  const result = constructor.apply(obj, args);
  return (result !== null && (typeof result === 'object' || typeof result === 'function'))
      ? result
      : obj;
}

四、JavaScript 作用域

4.1 基本概念

作用域指的是程序中定义的变量的可见范围,变量的作用域由它们在代码中声明的位置所决定。

4.2 作用域分类

4.2.1 全局作用域

  • 整个程序运行期间,任何地方都可以访问的作用域
  • 定义在该作用域的变量,在代码中任何地方都可以访问到

4.2.2 函数作用域

  • 在函数内部定义的变量,只能在该函数内部访问
  • 函数外部无法访问这些变量

4.2.3 块级作用域

  • ES6 新增的作用域类型
  • 在代码块({})中定义的变量,只能在该代码块内部访问
  • 代码块外部无法访问这些变量

4.3 作用域链

4.3.1 基本概念

作用域链是指由嵌套作用域组成的链式结构,用于解析变量名。当在某个作用域中访问变量时,JavaScript引擎会沿着作用域链向上查找,直到找到该变量或到达全局作用域。

4.3.2 工作原理

  • 每个函数在创建时都会创建一个作用域链
  • 作用域链包含当前函数的作用域和所有外层函数的作用域
  • 变量查找时,从当前作用域开始,逐级向外层作用域查找
  • 如果在全局作用域仍未找到变量,则返回 undefined

4.3.3 示例说明

var globalVar = "全局变量";

function outer() {
  var outerVar = "外层变量";
  
  function inner() {
    var innerVar = "内层变量";
    // 可以访问 innerVar、outerVar 和 globalVar
    console.log(innerVar, outerVar, globalVar);
  }
  
  inner();
  // 可以访问 outerVar 和 globalVar,但不能访问 innerVar
  console.log(outerVar, globalVar);
}

outer();
// 只能访问 globalVar
console.log(globalVar);

五、JavaScript 事件流

5.1 基本概念

事件流(Event Flow)指的是浏览器处理事件的方式,描述了事件在页面元素中传播的顺序,包括捕获阶段、目标阶段和冒泡阶段,这三个阶段从父节点到目标节点,再从目标节点到父节点的顺序依次发生。

5.2 事件流阶段

  1. 事件捕获阶段:事件从最外层的节点(文档对象)开始,逐级向下传播,直到到达事件的目标节点
  2. 目标阶段:事件到达了目标节点,触发目标节点上的事件处理函数
  3. 事件冒泡阶段:事件从目标节点开始,逐级向上传播,直到到达最外层的节点(文档对象)

5.3 事件控制方法

在 JavaScript 的事件流中,事件的传播是可以被停止或取消的。

  • event.stopPropagation():阻止事件的进一步传播,即阻止事件在捕获阶段或冒泡阶段的进一步传递
  • event.stopImmediatePropagation():阻止事件传播(同 stopPropagation),同时阻止同一元素上的其他事件处理程序被触发
  • event.preventDefault():取消事件的默认行为,但不阻止事件传播

5.4 事件委托(事件代理)

一种利用事件冒泡机制,将事件处理程序绑定到父元素上,由该父元素来统一处理其子元素所触发事件的编程技巧。

5.4.1 作用

通过事件委托,可以减少事件处理程序的数量,提高性能和代码的可维护性。


六、JavaScript 事件循环机制

6.1 基本概念

事件循环(Event Loop,事件轮询)是一种异步编程模型,用于处理 JavaScript 中的事件和回调函数,使单线程的 JavaScript 可以处理多个任务,实现异步编程。

6.2 事件循环机制

在浏览器中,JavaScript 事件循环机制由浏览器的事件循环负责执行。事件循环是一种机制,它不断地轮询任务队列,并将队列中的任务依次执行。

6.3 任务分类

JavaScript 中的任务分为两类:

6.3.1 宏任务

  • 花费时间较长的操作
  • 包括:定时器、事件回调、I/O 操作等
  • 当一个宏任务执行完毕后,JavaScript 引擎会检查是否存在未执行的微任务

6.3.2 微任务

  • 需要尽快执行的操作
  • 包括:Promise 的回调函数、MutationObserver 的回调函数等
  • 微任务可以使用 Promise 的 then 方法或者 MutationObserver 的 observe 方法注册

6.4 执行顺序

  1. 执行当前宏任务中的同步代码,直到遇到第一个宏任务或微任务
  2. 遇到宏任务,则将它添加到宏任务队列,继续执行同步代码
  3. 遇到微任务,则将它添加到微任务队列,继续执行同步代码
  4. 同步代码执行完后,检查微任务队列中是否有任务,有则执行直到清空
  5. 微任务清空后,拿出宏任务队列的第一个任务去执行
  6. 执行完这个宏任务后,检查微任务队列中是否有任务,有则执行直到清空
  7. 重复步骤5、6,直到宏任务和微任务队列都为空
  8. 当前事件循环结束,等待下一次事件的触发

七、闭包

7.1 基本概念

闭包是指在一个函数内部定义的函数,该函数可以访问外部函数的变量和参数。简单来说,闭包是有权访问另一个函数作用域中的变量的函数。

7.2 核心作用

  • 将变量、函数等数据私有化,避免命名冲突和变量污染
  • 函数执行完毕后,内部定义的变量和函数仍然存在于内存中,不会被自动回收,可以被其他函数继续访问和使用

7.3 实现原理

在内存中创建一个包含函数和变量的环境,当函数返回后,该环境仍然存在于内存中,因此可以被其他函数访问和使用。

7.4 使用场景

7.4.1 保存变量状态,实现数据私有化

// 实现私有计数器
function createCounter() {
  let count = 0; // 私有变量
  
  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
counter.getCount();  // 1
// 无法直接访问 count 变量

7.4.2 事件处理和回调函数

// 为多个按钮添加事件监听器
function setupButtons() {
  const buttons = document.querySelectorAll('.btn');
  
  for (let i = 0; i < buttons.length; i++) {
    // 使用闭包保存索引值
    buttons[i].addEventListener('click', (function(index) {
      return function() {
        console.log(`按钮 ${index + 1} 被点击了`);
      };
    })(i));
  }
}

7.4.3 实现缓存和记忆功能

// 记忆化斐波那契函数
function memoize(fn) {
  const cache = {};
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache[key]) {
      return cache[key];
    }
    
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

const fibonacci = memoize(function(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

fibonacci(10); // 55(首次计算)
fibonacci(10); // 55(从缓存获取)

7.4.4 解决循环中的异步问题

// 使用闭包解决 var 循环变量问题
for (var i = 0; i < 3; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(index); // 0, 1, 2(正确输出)
    }, 1000 * index);
  })(i);
}

// 使用 let 也可以解决(ES6)
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 0, 1, 2(正确输出)
  }, 1000 * i);
}

7.4.5 实现柯里化

// 柯里化示例
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    
    return function(...nextArgs) {
      return curried.apply(this, args.concat(nextArgs));
    };
  };
}

// 使用柯里化创建加法函数
const add = curry(function(a, b, c) {
  return a + b + c;
});

add(1)(2)(3); // 6
add(1, 2)(3); // 6
add(1)(2, 3); // 6

7.5 注意事项

在不正确使用闭包的情况下,会发生内存泄漏。为了避免内存泄漏,可以手动解除闭包,即将内部的引用删除。


总结

本文总结了 JavaScript 中的核心基础概念,包括:

  1. instanceof:基于原型链的实例检查机制
  2. 原型链:JavaScript 实现继承的基础机制
  3. new:对象实例创建的核心运算符
  4. 作用域:变量的可见范围和生命周期管理
  5. 事件流:事件在页面元素中的传播机制
  6. 事件循环:JavaScript 异步编程的核心模型
  7. 闭包:实现数据私有化和状态保存的重要特性

这些概念是 JavaScript 编程的基础,深入理解它们有助于编写更高效、更可靠的代码。希望本文能对您的 JavaScript 学习和复习有所帮助!

如果您对本文内容有任何疑问或补充,欢迎在评论区交流讨论~