知识梳理——JavaScript

这篇博客详细梳理了JavaScript的各个方面,从事件流、事件委托到作用域链、this的指向,再到函数的bind、apply、call的区别,以及箭头函数的特性。博主还讨论了new操作符的工作原理、JavaScript的节流和防抖技术,前端模块化(webpack与vite的区别)、以及Promise和async/await的使用。此外,还涵盖了闭包、数据类型、垃圾回收、异步加载JS的方法、页面渲染的优化策略等知识点,深入探讨了JavaScript的执行环境、作用域链以及原型链。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

all!!!

复习计划!!!

1. 说说前端中的事件流

HTML中与javascript交互是通过事件驱动来实现的,例如鼠标点击事件onclick、页面的滚动事件onscroll等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概念。

什么是事件流:事件流描述的是从页面中接收事件的顺序.

DOM2级事件流包括下面几个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段

addEventListener:addEventListener 是DOM2 级事件新增的指定事件处理程序的操作,这个方法接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。默认是false

IE只支持事件冒泡。

2. 如何让事件先冒泡后捕获

在DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获事件。

什么是事件冒泡

事件冒泡:开始时由最具体的元素接收,然后逐级向上传播到到 DOM 最顶层节点。

js怎么阻止事件冒泡

1.标准写法:利用事件对象里面的stopPropagation()方法

var div = document.querySelector('div')
div.addEventListener('click', function(e) {
    e.stopPropagation()
})

2.非标准写法:IE6-8 利用事件对象cancelBubble属性

var div = document.querySelector('div')
div.addEventListener('click', function(e) {
     e.cancelBubble = true // cancel 取消 Bubble 泡泡
 })

3.说一下事件委托(事件代理)

简介:事件委托指的是,不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。

举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。

好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。(减少内存消耗、动态绑定事件。)

什么是事件监听

addEventListener()方法,用于向指定元素添加事件句柄,它可以更简单的控制事件,语法为
element.addEventListener(event, function, useCapture);

  • 第一个参数是事件的类型(如 “click” 或 “mousedown”)

  • 第二个参数是事件触发后调用的函数

  • 第三个参数是个布尔值用于描述事件是冒泡还是捕获。该参数是可选的。事件传递有两种方式,冒泡和捕获,它定义了元素事件触发的顺序

如果将P元素插入到div元素中,用户点击P元素,在冒泡中,内部元素先被触发,然后再触发外部元素;在捕获中,外部元素先被触发,再触发内部元素

事件代理在捕获阶段的实际应用

可以在父元素层面阻止事件向子元素传播,也可代替子元素执行某些操作。

4.1 执行环境、作用域链、环境栈

1. 执行环境(又称执行上下文、作用域、环境)
执行环境:是js执行一段代码时的运行环境。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。【该对象保存了变量提升的内容】

函数执行环境:每个函数都有自己的执行环境。当执行流进入一个函数时(即调用该函数),函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

全局执行环境:是最外围的一个执行环境。全局执行环境被认为是window对象。

某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出 – 例如关闭网页或浏览器时,才会被销毁。)

作用域:就是在程序中定义变量和函数的可见范围,即作用域控制着变量和函数的可见性和生命周期。【大白话】

2. 作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一致延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
js经典面试题–变量提升、执行环境、作用域链

3.环境栈(又称调用栈)
1.在执行环境创建好后,JavaScript 引擎会将执行环境压入栈中,通常把这种用来管理执行环境的栈称为环境栈,又称调用栈。调用栈是 JavaScript 引擎追踪函数执行的一个机制。
2.调用栈是一种用来管理执行环境的数据结构,符合后进先出的规则

4.2 词法作用域、动态作用域(this)

1.词法作用域(静态的作用域)
词法作用域就是指作用域是由函数声明的位置来决定的,它是在代码编译阶段就决定好的,和函数是怎么调用的没有关系。(肉眼可见的函数外部的位置
JavaScript词法作用域

2.动态作用域(与this的调用有关)
动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。

5.1 改变函数内部this指针的指向函数(bind,apply,call的区别)

相同点:

  • 改变 this 指向

不同点:

  • call 和 apply 是立刻执行的,而 bind 是返回了一个新的函数,这个函数不会马上执行,需要用()调用,且返回的函数还可以传入新的参数——支持柯里化形式传参 fn(1)(2)
  • call 和 apply 的第一个参数都是:要改变指向的对象。但第二个参数不同,call 可以传递多个参数,是arg1, arg2……这种形式;而 apply 是数组

实现 call

面试 |call, apply, bind的模拟实现和经典面试题

Function.prototype.MyCall = function (context, ...args) {
    // this 指的是 fn (是 fn 调用的 MyCall)
    console.log(this);

    // context 就是 传入的obj
    // 如果 context 为空,默认为 window (参考call的处理方式)
    context = context || window;
    // 在context上扩展一个 fn 方法 (将函数挂载到 context 上)
    context.fn = this;
    // 调用 context.fn,此时fn的 this 指向 contetx
    context.fn(...args);
    // 删除对象上的属性 (不然 fn 这个方法就会留在调用的对象上)
    delete context.fn;
    return
};

let name = "张三";
let obj = {
    name: '李四'
};

function fn() {
    console.log(this.name, ...arguments);
}

fn.MyCall(null);
fn.MyCall(obj, 11, 22);

// fn.call(null);
// fn.call(obj, 11, 22);

console.log(obj); // obj 上没有挂载 fn (已经delete了)

实现 apply

Function.prototype.MyApply = function (context, args) {
    context = context || window;
    context.fn = this;
    if(args) {
        context.fn(...args);
    } else {
        context.fn();
    }
    delete context.fn;
    return
};

let name = "张三";
let obj = {
    name: '李四'
};

function fn() {
    console.log(this.name, ...arguments);
}

fn.MyApply(null);
fn.MyApply(obj,[11,22]);

// fn.apply(null);
// fn.apply(obj,[11,22]);

实现 bind

  1. 函数调用,改变 this
  2. 返回一个绑定 this 的函数
  3. 接收多个参数
  4. 支持柯里化形式传参 fn(1)(2)
Function.prototype.MyBind = function (context, ...args) {
    // 获取调用 MyBind 的函数主体 (fn)
    const _this = this;
    // 返回一个函数
    // 因为支持柯里化形式传参,我们需要再次获取存储参数 (即第二次传入的参数)
    return (...otherArgs) => {
        _this.call(context, ...args.concat(...otherArgs))
    }
};

var obj = {
    name: '张三'
};

function fn() {
    console.log(this.name, ...arguments)
}

// 使用 MyBind 调用
fn.MyBind(obj, 12, 23, 34, 56)(88, 99);

var a = fn.MyBind(obj, 12, 23, 34, 56);
a();


// // 使用原生 bind 调用
// fn.MyBind(obj, 12, 23, 34, 56)(88, 99);
//
// var aa = fn.bind(obj, 12, 23, 34, 56);
// aa();

5.2 this的指向问题

你不知道的js中关于this绑定机制的解析[看完还不懂算我输]

四种绑定规则的优先级:显式绑定和new绑定无法直接比较(会报错)
显式绑定 > 隐式绑定 > 默认绑定
new绑定 > 隐式绑定 > 默认绑定

this指的是当前作用域的实例对象

  1. 默认绑定: 在非严格模式下,默认绑定的 this 指向 window,严格模式下 this 指向 undefined
  2. 隐式绑定: 函数在调用位置,是否有上下文对象,如果有,那么 this 就会隐式绑定到这个对象
  3. 显示绑定: 在某个对象上强制调用函数,从而将 this 绑定在这个对象上,我们可以通过apply/call/bind将函数中的 this 绑定到指定对象上
  4. new绑定: 使用构造函数调用时,this 会自动绑定在 new 创建的对象上
  5. 箭头函数: 继承最近一层非箭头函数的 this(箭头函数的 this 一旦绑定了上下文,就不会被任何代码改变)

5.3 this 的设计缺陷以及应对方案

1. 缺陷:嵌套函数中的 this 不会从外层函数中继承
2. 解决方案:

  • 把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
  • 继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。

5.4 箭头函数与普通函数的区别

普通函数:
通过 function 关键字定义,this 指向取决于函数的调用方式

箭头函数:
1.使用" => "定义,语法更加简洁、清晰
2.箭头函数没有 prototype (原型),所以箭头函数本身没有this
箭头函数中的this是根据外层作用域来决定的,继承外层函数调用的this绑定

// 箭头函数
let a = () => {};
console.log(a.prototype); // undefined

// 普通函数
function a() {};
console.log(a.prototype); // {constructor:f}

3.箭头函数不会创建自己的this
箭头函数没有自己的this,箭头函数的this指向在定义(注意:是定义时,不是调用时)的时候继承自外层第一个普通函数的this。所以,箭头函数中 this 的指向在它被定义的时候就已经确定了,之后永远不会改变。

4.call | apply | bind 无法改变箭头函数中this的指向
call | apply | bind方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向。

5.箭头函数不能作为构造函数使用

6.箭头函数不绑定arguments,取而代之用rest参数…代替arguments对象,来访问箭头函数的参数列表

箭头函数与普通函数的区别?

6.js的new操作符做了哪些事情

1.用new关键字新建了一个空对象
2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
3.在构造函数内使用this为新对象添加属性
4.如果函数没有返回其他对象,那么 new 表达式中的函数调用会返回这个新对象

实现new

// fn:构造函数
function _new(fn, ...arg) {
    // 创建一个新对象,使用现有的对象作为 新创建的对象的原型 (__proto__)
    const newObj = Object.create(fn.prototype);
    // 构造函数内部的 this 被赋值给这个新对象 (即 this 指向新对象)
    const obj = fn.apply(newObj, arg);
    // 如果构造函数返回一个对象,则返回该对象;否则,返回刚创建的新对象
    return obj instanceof Object ? obj : newObj;
}

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.speak = function () {
        console.log('my name is ' + this.name)
    };

    // 当构造函数 return 了一个对象时, 返回该对象
    // return {
    //     content: '333333'
    // }
    
    // 当构造函数 return null 时, 返回新对象(newObj)
    // 因为 null instanceof Object === false
    // return null
}

const Amy = _new(Person,"Amy", 19);
console.log(Amy);   // Person {name: "Amy", age: 19, speak: ƒ}

【1】构造函数通常不使用 return 关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
【2】如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值