一、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 属性查找机制
当在对象中查找某个属性或方法时:
- 首先查找对象本身是否存在该属性或方法
- 如果不存在,JavaScript 引擎会去查找它的
__proto__(即构造函数的prototype) - 如果仍不存在,继续查找构造函数的
prototype的__proto__ - 以此类推,直到找到
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 执行流程
- 创建空对象:生成一个新的空对象,该对象的原型指向构造函数的原型对象
- 绑定 this:将构造函数的
this关键字绑定到刚创建的空对象上 - 执行构造函数:调用构造函数,执行其中的代码,通常用于初始化对象属性
- 返回结果:
- 如果构造函数显式返回一个对象(或函数),则返回该对象
- 否则,返回步骤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 事件流阶段
- 事件捕获阶段:事件从最外层的节点(文档对象)开始,逐级向下传播,直到到达事件的目标节点
- 目标阶段:事件到达了目标节点,触发目标节点上的事件处理函数
- 事件冒泡阶段:事件从目标节点开始,逐级向上传播,直到到达最外层的节点(文档对象)
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 执行顺序
- 执行当前宏任务中的同步代码,直到遇到第一个宏任务或微任务
- 遇到宏任务,则将它添加到宏任务队列,继续执行同步代码
- 遇到微任务,则将它添加到微任务队列,继续执行同步代码
- 同步代码执行完后,检查微任务队列中是否有任务,有则执行直到清空
- 微任务清空后,拿出宏任务队列的第一个任务去执行
- 执行完这个宏任务后,检查微任务队列中是否有任务,有则执行直到清空
- 重复步骤5、6,直到宏任务和微任务队列都为空
- 当前事件循环结束,等待下一次事件的触发
七、闭包
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 中的核心基础概念,包括:
- instanceof:基于原型链的实例检查机制
- 原型链:JavaScript 实现继承的基础机制
- new:对象实例创建的核心运算符
- 作用域:变量的可见范围和生命周期管理
- 事件流:事件在页面元素中的传播机制
- 事件循环:JavaScript 异步编程的核心模型
- 闭包:实现数据私有化和状态保存的重要特性
这些概念是 JavaScript 编程的基础,深入理解它们有助于编写更高效、更可靠的代码。希望本文能对您的 JavaScript 学习和复习有所帮助!
如果您对本文内容有任何疑问或补充,欢迎在评论区交流讨论~