【JavaScript】知识点总结

JavaScript面试题总结

  • JavaScript
    • 闭包
    • 作用域,作用域链
    • 执行栈,执行上下文
    • 内存泄漏 和 垃圾回收机制
      • 内存泄漏
      • 垃圾回收机制
    • 深拷贝和浅拷贝
    • 单线程,同步异步
      • 为什么JS是单线程的?
      • 同步异步
      • Promise
    • 变量提升
      • var let const 有什么区别
    • 模块化
      • exports和module.exports有什么区别?
      • ES6和commonjs的区别
      • require 和 import的区别?
    • 其它
      • 箭头函数
      • JavaScript中的严格模式
      • Ajax原理
      • 防抖和节流
      • 函数柯里化原理
      • requestAnimationFrame
      • JS常见的设计模式
      • 点击300ms 延迟问题
      • 如何实现上传视频?
      • setTimeOut
      • 暂时性死区
      • JS遍历数组的方法
      • 数组可以改变原数组的方法
      • foreach 和 map 有什么区别
      • 如何捕获浏览器关闭事件?
      • localstorage 怎么存储图片
      • 如何实现 localstorage 定时清除
      • 如何实现大文件上传?
      • 判断数据类型
      • web worker是干什么的?
      • jquery 如何实现链式调用
      • node 事件循环机制和浏览器事件循环机制有什么区别
      • Reflect
      • Object.keys和Object.getOwnPropertyNames有什么区别?
      • 如何配置rem
      • clientHeight、offsetHeight、scrollHeight有什么区别
      • BOM 和 DOM的区别
      • 倒计时用setimeout来实现还是setInterval
      • promise相对于async…await的优缺点
      • async/await 怎么抛出错误异常 ?
      • fetch优缺点
      • 秒传、分片传输、断点传输
      • e.target和e.currentTarget的区别
      • call,apply,bind的用法
      • ~arr 是什么用法
      • load和ready区别
      • for...of 和 for...in的区别
      • 如何实现数组的复制 ?
      • 如何用一次循环找到数组中两个最大的值 ?
      • 常见的兼容问题有哪些 ?
      • 常见的兼容问题的解决方法
      • 你了解构造函数吗 ? class 是什么 ? 两者有什么区别 ?
      • 在 JS 中如何阻止事件冒泡 ?
      • JavaScript如何存储cookie
      • symbol 你是怎么理解的 ?
      • 实现一个 add 方法 使计算结果能够满足如下预期: - add(1)(2)(3)() = 6 - add(1,2,3)(4)() = 10
      • less 和 sass 的区别 ?
      • 对 this 的理解, 三种改变 this 的方式 ?
      • 宏任务和微任务
      • JS性能优化的方式

JavaScript


闭包

在前端中,闭包(closure)指的是一个函数可以访问其外部的词法作用域中的变量,即使该外部作用域已经被销毁。具体来说,当一个函数在定义时捕获了其所在作用域中的变量,它就形成了一个闭包。这个函数可以在之后的执行过程中继续访问这些变量,即使它们不在当前作用域中。

以下是一个示例,展示了一个闭包的基本用法:

function makeCounter() {
  let count = 0;

  return function() {
    count++;
    console.log(count);
  };
}

const counter = makeCounter();

counter(); // 输出 1
counter(); // 输出 2
counter(); // 输出 3

在这个例子中,makeCounter 函数返回了一个匿名函数,该匿名函数可以访问 makeCounter 中的变量 count。每次调用 counter 函数时,它都会更新 count 的值并将其打印到控制台上。由于闭包的存在,counter 函数可以记住 count 的上一个值,并在之后的调用中使用它。

闭包还可以用来实现私有变量和私有方法。在 JavaScript 中,没有真正的私有变量和私有方法,但是可以通过闭包来模拟它们的行为。下面是一个例子,展示了如何使用闭包实现私有变量和私有方法:

function createPerson(name) {
  var age = 0;

  function getAge() {
    return age;
  }

  function setAge(newAge) {
    age = newAge;
  }

  return {
    getName: function() {
      return name;
    },
    getAge: getAge,
    setAge: setAge
  };
}

var person = createPerson("John");
console.log(person.getName()); // 输出 "John"
console.log(person.getAge()); // 输出 0
person.setAge(30);
console.log(person.getAge()); // 输出 30

在这个例子中,createPerson 函数返回一个对象,该对象有一个 getName 方法和两个闭包函数 getAgesetAgegetAgesetAge 函数可以访问 age 变量,这个变量是 createPerson 中定义的,并且只能通过 getAgesetAge 方法访问。由于 getAgesetAge 函数是闭包,所以它们可以访问 createPerson 的作用

作用域,作用域链

作用域(Scope)是指程序中定义变量的区域,它决定了变量的可见性和生命周期。在 JavaScript 中,作用域是静态的,即在代码编写阶段就已经确定好了,而不是在代码执行时动态确定。

作用域链(Scope Chain)是指由当前执行环境与上层环境中的变量对象(Variable Object)组成的链式结构。在 JavaScript 中,每个函数都有自己的作用域链,作用域链的顶端指向当前函数的变量对象,而作用域链的底端指向全局变量对象。在访问一个变量时,JavaScript 引擎会先从当前作用域的变量对象中查找该变量,如果没有找到,就继续向上层作用域的变量对象中查找,直到找到该变量或者到达全局作用域为止。

以下是一个简单的例子,展示了作用域和作用域链的概念:

var x = 1;

function foo() {
  var y = 2;
  console.log(x); // 输出 1
  console.log(y); // 输出 2
}

foo();
console.log(x); // 输出 1
console.log(y); // 报错:y is not defined

在这个例子中,变量 x 和函数 foo 都处于全局作用域中。当调用函数 foo 时,会创建一个新的执行环境,该执行环境会形成一个作用域链,其中包括函数 foo 的变量对象和全局变量对象。在函数 foo 内部,可以访问变量 x 和变量 y,因为它们都在作用域链中。在函数 foo 外部,可以访问变量 x,但是不能访问变量 y,因为它不在作用域链中。

需要注意的是,JavaScript 中没有块级作用域。在使用 var 声明变量时,变量的作用域是当前函数或全局作用域。在使用 letconst 声明变量时,变量的作用域是当前块级作用域,但是在函数内部定义的块级作用域变量,在函数外部无法访问。

执行栈,执行上下文

执行栈(Execution Context Stack),又称调用栈,是指存储函数调用的栈结构。在 JavaScript 中,每当执行一个函数时,就会创建一个对应的执行上下文,并将其推入执行栈中;当函数执行完毕时,对应的执行上下文就会从执行栈中弹出。执行栈采用后进先出(Last In First Out,LIFO)的原则,即最后一个进入栈中的执行上下文会最先被执行,最先进入栈中的执行上下文会最后被执行。

执行上下文(Execution Context)是指 JavaScript 代码执行时所在的环境。每个执行上下文都有自己的变量对象(Variable Object)、作用域链(Scope Chain)和 this 值(This Binding)。在执行栈中,当前正在执行的函数所对应的执行上下文位于栈顶,也就是说,执行上下文是以栈的形式组织起来的。

JavaScript 中有三种类型的执行上下文:

  1. 全局执行上下文(Global Execution
    Context):在代码执行前,首先会创建一个全局执行上下文,它是整个程序的根执行上下文,同时也是所有其他执行上下文的顶级执行上下文。全局执行上下文的变量对象是全局对象(Global Object)。
  2. 函数执行上下文(Function Execution Context):每当一个函数被调用时,都会创建一个对应的函数执行上下文。函数执行上下文的变量对象包含了函数的参数、函数内部声明的变量和函数内部声明的函数(如果有的话)。
  3. eval 执行上下文(Eval Function Execution Context):当执行 eval 函数时,会创建一个对应的
    eval 执行上下文。eval 执行上下文的变量对象是当前作用域的变量对象。

以下是一个简单的例子,展示了执行栈和执行上下文的概念:

function foo(a, b) {
  var c = 3;
  function bar(d) {
    console.log(a + b + c + d);
  }
  bar(4);
}
foo(1, 2);

在这个例子中,首先会创建一个全局执行上下文。当调用函数 foo 时,会创建一个函数执行上下文,它包含了参数 ab,以及变量 c 和函数 bar。当调用函数 bar 时,会创建一个新的函数执行上下文,它包含了参数 d。当函数 bar 执行完毕时,对应的函数执行上下文会从执行栈中弹出,然后继续执行函数 foo。最后,当函数 foo 执行完毕时,对应的函数执行上下文也会从执行栈中弹出,执行完成。

内存泄漏 和 垃圾回收机制

内存泄漏

内存泄漏是指程序在运行过程中,因为某些原因无法释放已经分配的内存,导致程序占用的内存越来越大,最终耗尽可用内存的现象。

在 JavaScript 中,内存泄漏通常发生在以下情况下:

  1. 意外的全局变量:如果没有使用 var、let 或 const 关键字声明变量,则变量会成为全局变量。如果全局变量不再使用但未被删除,它们将一直占用内存,从而导致内存泄漏。
  2. 闭包:如果在函数内部定义了一个函数,并将其作为返回值,这个函数将形成一个闭包。如果闭包中引用了外部函数的变量,但在外部函数执行结束后没有及时删除这个闭包,则闭包中引用的变量将一直被占用内存,导致内存泄漏。
  3. 循环引用:如果两个对象相互引用,而且它们都有一个属性指向对方,则形成了循环引用。如果这些对象不再被使用,但它们之间的引用关系没有断开,则它们将一直被占用内存,导致内存泄漏。
  4. 定时器:如果创建了定时器,但在定时器执行完毕后没有及时清除定时器,则定时器中引用的对象将一直被占用内存,导致内存泄漏。

解决内存泄漏的方法包括:

  1. 及时删除不再使用的全局变量。
  2. 及时释放不再使用的闭包。
  3. 避免循环引用。
  4. 及时清除定时器。
  5. 使用垃圾回收机制,及时回收不再使用的内存。JavaScript 的垃圾回收机制可以自动回收不再使用的内存,但它的执行时间是不确定的,因此我们需要避免创建过多的不必要对象,从而减少内存泄漏的可能性。

垃圾回收机制

垃圾回收是一种自动化的内存管理机制,它可以自动监控对象的分配和使用情况,以确定哪些对象没有被引用和使用,然后自动释放这些对象所占用的内存空间,从而防止内存泄漏和内存溢出等问题。

在 JavaScript 中,垃圾回收机制由 JavaScript 引擎自动管理。当 JavaScript 程序运行时,引擎会自动分配内存空间来存储对象和变量,当一个对象或变量不再被引用时,引擎会自动回收这些对象或变量所占用的内存空间。

JavaScript 的垃圾回收机制主要有两种实现方式:

  1. 标记清除(Mark and Sweep):这是最常用的垃圾回收算法。它的基本思想是通过标记所有活动对象,然后清除未标记对象的内存。这个算法可以处理循环引用的情况,但它需要在空闲时间运行,并且需要暂停整个程序的执行,因此可能会影响程序的性能。
  2. 引用计数(Reference Counting):这个算法的基本思想是为每个对象维护一个引用计数器,当有引用指向这个对象时,计数器加 1;当有引用被释放时,计数器减 1。当计数器为 0 时,说明该对象不再被使用,可以回收内存。这个算法的优点是可以及时回收不再使用的内存,但它不能处理循环引用的情况,因此可能会导致内存泄漏。

深拷贝和浅拷贝

在 JavaScript 中,对象和数组是引用类型,当我们对它们进行赋值、传参或返回时,实际上是将它们的引用复制了一份,而不是将它们本身复制一份。因此,在对对象和数组进行操作时,需要注意复制的是引用还是值。

浅拷贝和深拷贝就是针对对象和数组进行复制的两种方式。浅拷贝只复制了对象或数组的一层引用,而深拷贝则是递归地复制了整个对象或数组的所有属性和元素,包括嵌套的对象和数组。

浅拷贝复制的是对象或数组的引用,因此修改复制后的对象或数组会影响到原对象或数组,反之亦然。而深拷贝则复制了整个对象或数组,因此修改复制后的对象或数组不会影响到原对象或数组。

以下是浅拷贝和深拷贝的实现方式:

  1. 浅拷贝:
const obj = {a: 1, b: {c: 2}};
const copyObj = Object.assign({}, obj); // 使用Object.assign实现浅拷贝
console.log(copyObj); // {a: 1, b: {c: 2}}
console.log(obj.b === copyObj.b); // true,复制了一层引用
  1. 深拷贝:
const obj = {a: 1, b: {c: 2}};
const copyObj = JSON.parse(JSON.stringify(obj)); // 使用JSON.parse和JSON.stringify实现深拷贝
console.log(copyObj); // {a: 1, b: {c: 2}}
console.log(obj.b === copyObj.b); // false,完全复制了对象和数组的所有属性和元素
function deepCopy(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  let result = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepCopy(obj[key]);
    }
  }
  return result;
}

需要注意的是,深拷贝会递归地遍历整个对象或数组的所有属性和元素,如果遇到循环引用或函数等特殊类型的值,可能会出现意料之外的结果。此外,使用 JSON.parse 和 JSON.stringify 实现深拷贝时,会将函数和 undefined 类型的值忽略掉,因此如果对象中包含函数或 undefined 类型的值,可能会丢失这些值。

单线程,同步异步

JavaScript 是一门单线程语言,这意味着它只有一个主线程(也称为 UI 线程)来处理所有的任务。因此,JavaScript 中的代码是按照一定的顺序依次执行的,也就是同步执行。

为什么JS是单线程的?

由于 JavaScript 本身是单线程的,因此可以避免一些多线程并发带来的问题,例如竞态条件、死锁等。此外,单线程还可以更好地保证代码的可读性和可维护性,避免多线程带来的复杂性。

当然,单线程也带来了一些限制,例如阻塞会导致 UI 线程被占用,无法响应用户的交互操作,因此需要采用异步的方式来处理耗时的任务。另外,由于 JavaScript 是单线程的,因此如果某个任务占用了大量的时间,就会阻塞后续任务的执行,这时可以通过 Web Workers 来实现多线程执行。

同步异步

同步执行指的是代码是按照书写顺序依次执行,每一行代码执行完成后再执行下一行代码,直到程序执行完毕。如果当前的任务没有执行完成,那么主线程就会一直等待,直到当前任务执行完成。

异步执行指的是在任务执行时,可以同时执行其他任务,而不需要等待当前任务执行完成。JavaScript 中的异步执行一般是通过回调函数、事件、定时器等方式实现的。例如:

console.log('start');

setTimeout(() => {
  console.log('timer');
}, 2000);

console.log('end');

在上面的代码中,setTimeout() 方法是一个异步方法,它会在 2 秒后执行回调函数中的代码,而不会影响到后面的代码执行。因此,上面的代码会先打印出 start,然后立即打印出 end,最后在 2 秒后打印出 timer。

需要注意的是,JavaScript 中的异步执行并不是多线程执行,只是通过事件循环机制实现了异步执行。具体来说,当遇到一个异步任务时,JavaScript 会将它加入到任务队列中,并立即执行下一行代码,当主线程空闲时,事件循环机制会从任务队列中取出一个任务并执行,直到任务队列为空。

另外,需要注意的是异步执行中的回调函数有可能会产生回调地狱(也称为 callback hell)的问题,即回调函数嵌套过深,代码难以维护和理解。为了解决这个问题,JavaScript 中引入了 Promise、async/await 等方式来处理异步任务。

Promise

Promise是一种语法工具,用于简化异步编程。它本质上是一个状态机,有三种状态:pending、fulfilled和rejected。Promise对象的状态可以通过resolve和reject方法从pending(未决定)状态转换为fulfilled(已成功)或rejected(已失败)状态。当Promise对象的状态从pending转换为fulfilled或rejected状态时,可以调用then或catch方法来处理异步操作的结果。then方法返回一个新的Promise对象,可以链式调用来组织异步操作。Promise的优点是可以避免回调函数的嵌套和复杂性。

变量提升

变量提升是JavaScript中的一个概念,指的是在代码执行之前,JavaScript引擎会先将变量和函数的声明提升到当前作用域的最顶部,这样在代码中的任何位置都可以访问到这些变量和函数。

对于变量声明,如果是用var关键字声明的变量,它们会被初始化为undefined,然后才会执行赋值操作。对于函数声明,它们会被完整地提升到作用域顶部,因此可以在函数声明之前调用这些函数。

例如,下面的代码中,变量a和函数b都被提升到了代码块的最顶部:

console.log(a);  // undefined
console.log(b);  // function b() {}

var a = 1;
function b() {
  console.log('function b is called');
}

因此,即使在变量和函数声明之前访问它们,也不会抛出ReferenceError错误,但变量的值会是undefined。

var let const 有什么区别

var、let和const都是用来声明变量的关键字,但它们之间有一些区别:

var存在变量提升,而let和const不存在变量提升。这意味着,在使用var声明变量时,变量会被提升到函数或全局作用域的顶部,可以在声明之前使用。而使用let和const声明变量时,如果在声明之前使用这些变量,会抛出ReferenceError错误。

var可以重复声明同名变量,而let和const不能重复声明。在使用var声明变量时,如果多次使用var关键字声明同一个变量名,JavaScript引擎会忽略后面的声明,仍然使用第一个声明的变量。而使用let和const声明变量时,如果再次使用相同的变量名声明变量,会抛出SyntaxError错误。

let和const都是块级作用域,而var不是。在使用let和const声明变量时,变量的作用域仅限于当前的代码块(花括号{}括起来的部分)。而使用var声明变量时,变量的作用域可以是函数作用域或全局作用域。

const声明的变量是常量,不可修改其值。使用const声明变量时,变量的值不能被重新赋值。而使用let和var声明的变量可以重新赋值。

总之,使用var声明变量已经不推荐了,推荐使用let和const来声明变量,让代码更加严谨、清晰。如果需要定义一个常量或者不希望变量被修改,应该使用const来声明变量。

模块化

使用模块化的主要目的是为了提高代码的可维护性、可复用性和可扩展性,避免代码的重复和混乱,减少全局变量的使用。

在 JavaScript 中,实现模块化的方式主要有以下几种:

  1. AMD (Asynchronous Module Definition):异步模块定义,是一种在浏览器端使用的模块化规范,可以异步加载模块,避免了阻塞页面加载。它使用 define 来定义模块,require 来加载模块。
  2. CommonJS:这是一种服务器端的模块化方案,主要用在 node 开发上,每个文件就是一个模块,每个文件都有自己的一个作用域。通过module.exports 暴露 public 成员。
  3. ES6 Module:ES6 模块的设计思想是尽量的 静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。
    在 ES6 中,我们使用 export 关键字来导出模块,使用 import 关键字来引入模块。
  4. UMD:UMD 是 (Universal Module Definition) 通用模块定义 的缩写。UMD 是 AMD 和 CommonJS 的一个糅合。AMD 是浏览器优先,异步加载;CommonJS 是服务器优先,同步加载。
    既然要通用,怎么办呢?那就先判断是否支持 node 的模块,支持就使用 node;再判断是否支持 AMD,支持则使用 AMD 的方式加载。这就是所谓的 UMD。
  5. CMD 是 (Common Module Definition) 公共模块定义 的缩写。CMD 可能是在 CommonJS 之后抽象出来的一套模块化语法定义和使用的标准。
    在 CMD 规范中,一个模块就是一个文件。与 AMD 不同的是,CMD 更加强调模块的延迟执行(Lazy Execution),即模块在需要时才会被执行。

总的来说,模块化可以让代码更加清晰、可维护和可重用,而不同的模块化方案有各自的优缺点,需要根据实际需求来选择合适的方案。

exports和module.exports有什么区别?

module.exports 是 Node.js 中真正的导出对象,而 exports 只是对 module.exports 的一个引用。如果直接对 exports 赋值,会改变它的指向,导致导出失败。所以,如果要导出一个对象,应该使用 module.exports,而如果要导出一个单独的属性或方法,可以使用 exports

ES6和commonjs的区别

ES6 和 CommonJS 都是 JavaScript 中的模块化规范,但它们有一些不同之处:

  1. 语法不同:ES6 使用 import/export 关键字来实现模块化(异步加载),而 CommonJS 使用 require/module.exports (同步加载)来实现模块化。
  2. 执行时机不同:ES6 模块是在编译时静态分析的,因此可以在编译阶段进行优化,而 CommonJS 模块是在运行时加载的,无法进行静态优化。
  3. 可变绑定不同:ES6模块的变量是不可变的,只能通过export/import暴露和引用,而CommonJS模块的变量是可变的,因此在模块中对变量的修改会影响其他模块。
  4. 循环依赖处理不同:ES6 可以处理循环依赖,而 CommonJS 在处理循环依赖时会出现问题。
  5. 支持的环境不同:ES6 模块可以在浏览器环境和 Node.js 环境中使用,而 CommonJS 模块主要是在 Node.js 环境中使用的。

总的来说,ES6 模块在编译时静态分析、作用域静态确定、支持循环依赖处理等方面具有优势,但在浏览器环境中的兼容性不如 CommonJS。而 CommonJS 则在动态加载和动态作用域等方面具有优势,但在循环依赖处理方面存在问题。

require 和 import的区别?

requireimport 都是用来导入模块的关键字,但它们有几个不同点:

  1. 语法不同:require 是 CommonJS 规范中的关键字,使用 require 时需要传入模块路径字符串,如 const foo
    = require(‘./foo.js’);
    import 是 ES6 中的关键字,使用 import 时需要使用花括号包裹需要导入的变量或函数,如 import {foo} from ‘./foo.js’
  2. 静态/动态:require 是动态的,可以在运行时根据条件导入不同的模块;而 import 是静态的,它只能在模块顶部使用,无法在运行时动态导入。
  3. 导出方式不同:require 导入的是整个模块对象,可以通过该对象访问模块中的变量和函数;而 import 可以直接导入模块中的特定变量或函数,也可以重命名导入的变量或函数。
  4. require 是同步加载模块,会阻塞后续代码的执行,而 import 是异步加载模块,不会阻塞后续代码的执行,可以提高代码执行的效率。

需要注意的是,虽然 import 可以在大多数现代浏览器和 Node.js 版本中使用,但仍有一些老旧的浏览器和 Node.js 版本不支持 import,因此在某些情况下仍需要使用 require

其它

箭头函数

箭头函数是 ES6 中新增的一种函数定义方式,它通过简洁的语法和特定的行为,使得编写 JavaScript 代码更加方便和易读。

箭头函数的基本语法如下:

(parameter1, parameter2, ..., parameterN) => { statements }

其中,(parameter1, parameter2, …, parameterN) 是函数的参数列表,{ statements } 是函数体,箭头 => 表示函数的定义。

与普通函数不同的是,箭头函数有以下特点:

  1. 箭头函数没有自己的 this,它的 this 始终指向它外层的第一个普通函数的 this,也就是说箭头函数的 this 是词法作用域,而普通函数的 this 是在运行时动态绑定的。
  2. 箭头函数没有自己的 arguments,可以使用 rest 参数代替。
  3. 箭头函数不能使用 new 关键字来实例化对象,因为箭头函数没有自己的 this,也没有 prototype 属性。
  4. 箭头函数的返回值可以不加 return 关键字,如果函数体只有一条语句,则该语句的值即为返回值。

箭头函数适合编写简单的函数,如简单的回调函数或者简单的函数式编程,它可以减少代码量,提高代码可读性。但是需要注意的是,在某些场景下,使用箭头函数可能会导致代码的可读性变差,因此需要根据具体的场景和需求选择合适的函数定义方式。

JavaScript中的严格模式

1.禁止不标准的全局变量(未使用var)
2.禁止使用with语句
3.禁止使用eval
4.禁止this关键字指向全局对象
5.禁止在函数内部遍历调用栈
6.禁止删除变量
7.对象不能有重命名属性
8.函数不能有重命名参数
9.禁止八进制表示法
10.不能对arguments赋值
11.arguments不再追踪参数的变化
12.禁止使用arguments.callee
13.严格模式只允许在全局作用域或函数作用域的顶层声明函数。也就是说,不允许在非函数的代码块内声明函数。
14.不能使用某些保留字:implements, interface, let, package, private, protected, public, static, yield。

为了启用严格模式,可以在JavaScript文件的开头添加如下语句:

"use strict";

也可以在函数体内部的开头添加该语句,只对该函数内部的代码启用严格模式。

Ajax原理

当页面需要获取服务端的数据时,使用 AJAX 可以在不刷新整个页面的情况下向服务器发送请求并接收响应数据,从而更新部分页面内容。

AJAX 的原理是通过 XMLHttpRequest 对象向服务器发送异步请求,获取到响应数据后在页面上进行更新。当页面需要与服务器进行通信时,浏览器会创建一个 XMLHttpRequest 对象,然后通过该对象向服务器发送请求,服务器接收请求后进行处理,最后将处理结果返回给浏览器。浏览器在接收到响应后,解析响应数据,并将数据更新到页面中。

在发送请求前,我们需要配置 XMLHttpRequest 对象,指定请求的类型、地址、数据等信息。一般情况下,我们使用的是 GET 或 POST 请求,GET 请求用于获取数据,而 POST 请求用于提交数据。发送请求时,我们可以指定请求的参数和头信息。在接收响应后,我们需要解析响应数据,并根据需要进行相应的页面更新。

AJAX 的优点在于可以在不刷新整个页面的情况下更新部分页面内容,从而提高页面的交互性和用户体验。同时,使用 AJAX 还可以减轻服务器的压力,因为可以部分地更新页面内容而不是每次都刷新整个页面。

> ---代码示例:
function ajax(method, url, data, success, fail) {
  var xhr = new XMLHttpRequest();

  xhr.onreadystatechange = function() {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        if (success) {
          success(xhr.responseText);
        }
      } else {
        if (fail) {
          fail(xhr.status);
        }
      }
    }
  };

  xhr.open(method, url, true);
  xhr.setRequestHeader('Content-Type', 'application/json');
  xhr.send(JSON.stringify(data));
}

// GET 请求
ajax('GET', '/api/data', null, function(response) {
  console.log(response);
}, function(error) {
  console.error(error);
});

// POST 请求
var data = { name: 'John', age: 25 };
ajax('POST', '/api/data', data, function(response) {
  console.log(response);
}, function(error) {
  console.error(error);
});

这个例子中,ajax 函数接受五个参数,分别是请求方法、请求 URL、请求数据、请求成功回调函数和请求失败回调函数。在函数中,首先创建一个 XMLHttpRequest 对象,然后设置其 onreadystatechange 属性,用于监听请求状态的变化。当请求状态变为 XMLHttpRequest.DONE 时,判断请求状态码是否为 200,如果是,则调用请求成功回调函数并传入响应数据,否则调用请求失败回调函数并传入错误状态码。最后,通过 xhr.open() 方法设置请求方法、URL 和是否异步发送请求,通过 xhr.setRequestHeader() 方法设置请求头,最后通过 xhr.send() 方法发送请求。

需要注意的是,在这个例子中,我们将请求数据转换成 JSON 字符串,并将请求头设置为 application/json,因此服务器需要能够正确解析这种格式的请求。如果服务器期望其他格式的请求数据,需要相应地修改代码。

此外,需要注意的是,由于 AJAX 请求是异步执行的,因此无法在请求函数外部直接获取到响应数据,而需要将请求成功的回调函数作为参数传入请求函数中。如果需要在请求成功后修改页面内容,需要在请求成功回调函数中进行操作。

防抖和节流

防抖和节流都是前端开发中常用的优化性能的技术。

防抖(debounce)的原理是在一段时间内,只有最后一次事件触发会被执行,而前面的事件会被忽略。比如说在输入框中输入内容,为了减少请求次数,可以将触发请求的函数用防抖函数包裹起来,以确保在用户输入完成一段时间后才发起请求,避免了频繁的请求。

节流(throttle)的原理是在一定时间内,无论事件触发多少次,只会执行一次。比如说在用户滚动页面的时候,为了减少页面的性能损耗,可以将执行滚动操作的函数用节流函数包裹起来,以确保在一定时间内只执行一次,避免了频繁的操作。

防抖和节流的实现方式可以使用原生 JavaScript 或者第三方库来实现。例如,在原生 JavaScript 中可以通过 setTimeout 和 clearTimeout 来实现防抖和节流,而在第三方库如 Lodash 中也提供了相应的方法来实现这两个功能。
手写代码如下:

// 防抖
function debounce(func, wait) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(function() {
      func.apply(context, args);
    }, wait);
  };
}

// 节流
function throttle(func, wait) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(function() {
        func.apply(context, args);
        timeout = null;
      }, wait);
    }
  };
}
//节流函数原理
/*具体来说,当第一次触发事件时,函数会立即执行,之后会在指定的时间间隔内,忽略所有事件,
直到时间间隔结束,才会执行下一次函数。在这个过程中,如果有新的事件触发,就会重新开始
计时,等待指定的时间间隔结束后,才会执行函数。

这个代码的实现方式是利用了闭包,通过设置一个定时器来控制函数的执行时间。
具体来说,当第一次触发事件时,会创建一个定时器,等待指定的时间间隔结束
后,执行函数,并将定时器清空。如果在这个过程中又触发了事件,就会重新创
建一个定时器,等待指定的时间间隔结束后,再次执行函数。*/

这里的 debouncethrottle 都是返回一个新的函数,用于包装原有的函数。在调用 debouncethrottle 函数时,传入需要包装的函数以及相应的等待时间,即可得到一个新的函数。调用新函数时,会限制函数的调用次数以及频率。

函数柯里化原理

函数柯里化是一种将接受多个参数的函数转化为接受单一参数(最初函数的第一个参数)的函数,并返回接受余下参数且返回结果的新函数的技术。

其原理可以简单概括为:将一个需要多个参数的函数转换成一系列需要单一参数的函数,并且将这些函数串联起来,使其返回结果的方式就像是一次性地调用原始的函数。

函数柯里化可以通过两种方式实现:闭包和递归。

闭包实现的函数柯里化示例如下:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

递归实现的函数柯里化示例如下:

function curry(fn, ...args) {
  return (...args2) =>
    args.length + args2.length >= fn.length
      ? fn(...args, ...args2)
      : curry(fn, ...args, ...args2);
}

这两种实现方式本质上都是将多个参数的函数拆解成单一参数的函数,不同的是闭包方式通过返回一个函数来达到柯里化的效果,而递归方式则通过返回一个匿名函数来达到柯里化的效果。

requestAnimationFrame

requestAnimationFrame 是 HTML5 新增的 API,用于替代旧版 setIntervalsetTimeout。它是浏览器提供的一种优化性能的方式,能够在下一次重绘之前执行指定的函数,从而避免了一些卡顿和掉帧的问题。

具体来说,当我们使用 requestAnimationFrame 时,浏览器会在下一次重绘之前执行指定的函数,这样可以保证在更新画面时不会出现掉帧的现象,同时也能够最大程度地减少 CPU 和 GPU 的负担,提高页面性能。

使用 requestAnimationFrame 的语法如下:

window.requestAnimationFrame(callback);

其中,callback 是要执行的函数,它接受一个 DOMHighResTimeStamp 类型的参数,表示从页面加载开始到当前执行的时间间隔,单位是毫秒。

需要注意的是,requestAnimationFrame 返回的是一个 ID,可以通过调用 cancelAnimationFrame 方法来取消该 ID 对应的回调函数的执行。

let id = window.requestAnimationFrame(callback);
window.cancelAnimationFrame(id);

总的来说,requestAnimationFrame 的优势在于它能够在下一次重绘之前执行指定的函数,从而最大限度地避免了卡顿和掉帧的问题,同时也能够提高页面性能。

JS常见的设计模式

JavaScript 中常见的设计模式有很多,其中一些比较常见的包括:

  1. 单例模式(Singleton Pattern):保证一个类只有一个实例,并提供一个访问它的全局访问点。
  2. 工厂模式(Factory Pattern):定义一个用于创建对象的接口,让子类决定实例化哪一个类。
  3. 观察者模式(Observer Pattern):定义了对象之间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知。
  4. 发布-订阅模式(Publish-Subscribe Pattern):定义了对象间一种一对多的关系,使得多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖它的对象都会得到通知。
  5. 代理模式(Proxy Pattern):为一个对象提供一个代用品或占位符,以便控制对它的访问。
  6. 装饰者模式(Decorator Pattern):动态地给一个对象添加一些额外的职责,就增加功能来说,装饰者模式比生成子类更为灵活。
  7. 命令模式(Command Pattern):将请求封装成对象,从而可以使用不同的请求、队列或者日志来参数化其他对象。

点击300ms 延迟问题

点击 300ms 延迟问题是指在移动设备的浏览器中,当用户点击屏幕上的某个元素时,浏览器会等待大约 300ms 的时间来判断用户是否要双击缩放页面,这个等待时间会给用户带来一定的响应延迟,从而影响用户体验。

这个问题的根本原因是由于浏览器需要等待一段时间来判断用户的点击行为,所以在点击后需要等待一段时间才能响应。为了解决这个问题,可以采用以下几种方法:

  1. 使用 meta 标签来禁用缩放功能

     <meta name="viewport" content="width=device-width, user-scalable=no">
    

    这样就可以禁用浏览器的缩放功能,从而消除 300ms 的延迟。

  2. 使用 FastClick
    FastClick 是一个轻量级的库,用于消除点击延迟问题。它的原理是通过绑定 touchstartclick 事件来模拟点击,从而消除延迟。

  3. 使用 touch-action 样式属性
    touch-action 样式属性用于指定元素的触摸事件处理方式。可以将其设置为 manipulation,表示元素将被浏览器优化为移动设备上的默认操作行为,从而消除延迟。

    .my-element {
      touch-action: manipulation;
    }
    

以上方法都可以解决点击 300ms 延迟问题,具体采用哪种方法,可以根据自己的实际情况进行选择。

如何实现上传视频?

实现上传视频的过程可以分为以下几步:

  1. 确定上传视频的后端接口 需要先和后端工程师确认接口的定义和参数,比如上传接口的URL、请求方法、参数名称、参数格式等。

  2. 创建文件上传表单 在前端页面中创建一个表单,通过该表单选择本地的视频文件,用于上传到后端。表单中需要包含一个 input 标签,type 属性设置为 file,同时设置 accept 属性为允许上传的视频格式。

  3. 监听上传事件 当用户选择好本地的视频文件后,可以通过 JavaScript 监听 change
    事件,在事件处理函数中获取用户选择的文件对象。

  4. 使用 FormData 将文件数据提交到后端 将用户选择的文件对象作为参数创建 FormData 对象,并将其通过 Ajax 发送到后端接口。

  5. 在后端接口中处理上传的视频文件 在后端接口中处理上传的视频文件,比如保存文件、获取文件的 URL 等。最终返回上传成功或失败的响应结果。

  6. 处理上传结果 在前端页面中接收后端返回的上传结果,并进行相应的提示或跳转处理。

需要注意的是,由于视频文件通常较大,所以上传过程需要考虑用户的网络状况,以及后端的上传限制。为了更好的用户体验,可以在上传过程中展示进度条或上传进度百分比等信息。

setTimeOut

setTimeout 是 JavaScript 中的一个函数,它可以在指定的时间后执行一次函数。
它的语法如下:

setTimeout(func, delay, param1, param2, ...)

其中,func 是要执行的函数,delay 是延迟的时间(毫秒),param1, param2, … 是可选的参数,会传递给函数。

setTimeout 返回一个计时器 ID,可以用于在指定的延迟时间后取消执行。

一般来说,setTimeout 常用于实现延迟执行或者定时执行的效果,例如实现一个倒计时功能、设置定时任务等。

需要注意的是,setTimeout 并不是准确的定时器,它可能因为一些因素(例如执行的代码太长、系统负载过高等)而导致延迟时间不准确,因此在需要精确计时的场景中,可以考虑使用 requestAnimationFrame 或者 setInterval

暂时性死区

暂时性死区是指在 ES6 中使用 let 和 const 声明变量时,变量在代码块内部被绑定(即在变量声明前不能访问),这个时期就称为暂时性死区。在这个阶段,如果试图访问该变量,JavaScript 引擎就会抛出一个 ReferenceError 异常。

这个特性的目的是为了解决 var 变量提升所带来的问题,从而使 JavaScript 更加严谨。

以下是一个暂时性死区的例子:

function example() {
  console.log(foo); // ReferenceError
  let foo = "bar";
}

在该函数内部,foo 在声明前被调用,因此会抛出 ReferenceError 异常。

JS遍历数组的方法

在 JavaScript 中,遍历数组有多种方法。以下是其中一些常见的方法:

  1. for 循环
    使用 for 循环可以遍历数组,如下所示:

    const arr = [1, 2, 3];
    for (let i = 0; i < arr.length; i++) {
      console.log(arr[i]);
    }
    
  2. forEach()
    forEach() 是一个内置的数组方法,用于遍历数组,它可以接受一个回调函数作为参数,对数组中的每个元素执行回调函数,如下所示:

    const arr = [1, 2, 3];
    arr.forEach(function(item) {
      console.log(item);
    });
    
  3. map()
    map() 是一个内置的数组方法,用于遍历数组并返回一个新的数组,它可以接受一个回调函数作为参数,对数组中的每个元素执行回调函数并返回一个新的数组,如下所示:

    arr.map(function(item) {   return item * 2; }); console.log(newArr);
    // [2, 4, 6] ```
    
    
  4. filter()
    是一个内置的数组方法,用于遍历数组并返回一个新的数组,它可以接受一个回调函数作为参数,对数组中的每个元素执行回调函数并返回一个布尔值,如果回调函数返回
    true,则将该元素添加到新的数组中,如下所示:

    arr.filter(function(item) {   return item % 2 === 0; });
    console.log(newArr); // [2, 4] ```
    
    
  5. reduce()
    是一个内置的数组方法,用于遍历数组并返回一个值,它可以接受一个回调函数和一个初始值作为参数,对数组中的每个元素执行回调函数并返回一个累加值,如下所示:

    arr.reduce(function(acc, item) {   return acc + item; }, 0);
    console.log(sum); // 15 ```
    
    
  6. for…of 循环,使用 for…of 循环可以遍历数组,如下所示:

    const arr = [1, 2, 3];
    for (const item of arr) {
      console.log(item);
    }
    

数组可以改变原数组的方法

ES5:push(), pop(), shift(), unshift(), reverse(),sort(),splice()

ES6:copyWithin(),fill()

foreach 和 map 有什么区别

foreach 没有返回值,一般如果用来遍历修改原数组的话可以用 foreach 方法

如何捕获浏览器关闭事件?

可以使用beforeunload事件来捕获浏览器关闭事件。当用户关闭或刷新页面时,浏览器会触发该事件。可以将事件处理程序添加到window对象上,以便在浏览器关闭或刷新时执行某些操作。

例如,可以使用以下代码来显示一个确认框,询问用户是否要离开页面:

window.addEventListener('beforeunload', function(event) {
  event.preventDefault();
  event.returnValue = '';
});

在这里,我们添加了一个事件监听器来捕获beforeunload事件。我们使用preventDefault方法来防止浏览器默认行为,然后设置event.returnValue属性为空字符串,以显示确认对话框。

请注意,beforeunload事件的用法受到浏览器的限制,有些浏览器可能会忽略event.returnValue属性,因此可能无法完全阻止页面关闭。

localstorage 怎么存储图片

LocalStorage 本身不能直接存储图片,因为它只能存储字符串类型的数据,但是可以将图片转化为字符串类型,存储在 LocalStorage 中。可以使用以下两种方式实现:

1.使用 Canvas 将图片转换为 Base64 编码的字符串,再存储在 LocalStorage 中:

// 获取图片元素
const img = document.querySelector('img');
// 创建 canvas 元素
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置 canvas 元素的宽高
canvas.width = img.width;
canvas.height = img.height;
// 在 canvas 上绘制图片
ctx.drawImage(img, 0, 0, img.width, img.height);
// 将 canvas 转换为 Base64 编码的字符串
const base64 = canvas.toDataURL('image/png');
// 将字符串存储在 LocalStorage 中
localStorage.setItem('myImage', base64);

2.使用 FileReader 将图片转换为 Base64 编码的字符串,再存储在 LocalStorage 中:

// 获取图片元素
const img = document.querySelector('img');
// 创建 FileReader 实例
const reader = new FileReader();
// 读取图片
reader.readAsDataURL(img);
// 监听 onload 事件
reader.onload = function() {
  // 获取 Base64 编码的字符串
  const base64 = reader.result;
  // 将字符串存储在 LocalStorage 中
  localStorage.setItem('myImage', base64);
};

需要注意的是,由于 LocalStorage 的容量有限,存储大量图片会占用过多的空间,建议只存储较小的图片或者使用其他的存储方式;如可以使用 base64 编码方式将图片转换成字符串进行存储,而不需要将图片保存到服务器上。

如何实现 localstorage 定时清除

可以使用定时器和 localStorage.removeItem() 方法来实现定时清除 localStorage 中的数据。

以下是一个简单的实现示例,可以在每次页面加载时启动定时器:

function setLocalStorage(key, value, time) {
  localStorage.setItem(key, JSON.stringify(value));
  if (time) {
    // 计算过期时间
    var expireTime = new Date().getTime() + time * 1000;
    // 将过期时间也存储到localStorage中
    localStorage.setItem(key + '_expireTime', expireTime);
    // 启动定时器,到期后删除数据
    var timer = setInterval(function() {
      var expireTime = parseInt(localStorage.getItem(key + '_expireTime'));
      if (!expireTime || new Date().getTime() > expireTime) {
        localStorage.removeItem(key);
        localStorage.removeItem(key + '_expireTime');
        clearInterval(timer);
      }
    }, 1000);
  }
}

使用方法:

setLocalStorage('data', {name: 'Tom', age: 18}, 10);

在这个示例中,我们增加了一个 time 参数,用于指定数据的过期时间,单位为秒。如果传入了 time 参数,我们会先将数据存储到 localStorage 中,并同时将数据的过期时间也存储到 localStorage 中,然后启动一个定时器,每隔1秒检查一次数据是否过期,如果过期则将数据从 localStorage 中删除,并停止定时器。

需要注意的是,这种方法只能实现定时清除 localStorage 中的数据,而无法保证在指定时间内清除数据。如果需要在指定时间内自动清除数据,可以考虑使用 IndexedDB 等浏览器提供的数据库来实现。

如何实现大文件上传?

实现大文件上传可以使用以下方法:

  1. 分片上传:将大文件分成小块,然后每次上传一小块,上传完成后再上传下一块,直到整个文件上传完成。这样可以避免上传失败或者网络中断等问题导致整个文件需要重新上传。
  2. 断点续传:将大文件分成多个小块,然后每次上传一小块,同时记录已上传的块的位置,如果上传失败或者中断,下次上传时可以从上次的位置继续上传,避免重新上传整个文件。
  3. WebSocket上传:使用 WebSocket 实现文件上传,相比于 HTTP 传输具有更高的实时性和可靠性。在上传大文件时,WebSocket 可以提供更快的传输速度和更稳定的连接。

判断数据类型

web worker是干什么的?

Web Worker 是一项 HTML5 的新技术,用于在浏览器的后台运行 JavaScript 代码,不会影响到主线程的运行,从而提高页面的性能和响应速度。Web Worker 能够将复杂、计算密集的任务分离出来,在独立的线程中运行,从而避免了这些任务对主线程的阻塞和卡顿。

Web Worker 主要用于执行长时间运算、数据处理、图像操作、网络请求等耗时的任务,它可以使这些任务在后台运行,不会阻塞主线程,从而提高页面的响应速度和用户体验。

Web Worker 的工作原理是通过在主线程中创建一个 Worker 对象,通过这个对象的 postMessage 方法向 Worker 线程发送消息,然后在 Worker 线程中通过 onmessage 事件监听主线程发送的消息,并且通过 postMessage 方法向主线程发送处理结果。

jquery 如何实现链式调用

jQuery 实现链式调用的原理是每个 jQuery 方法都会返回 jQuery 对象,这样就可以继续调用其它 jQuery 方法。具体实现是在每个方法的末尾都返回 this 对象,即 jQuery 对象本身。

例如:

$('div').addClass('active').css('color', 'red').hide();

以上代码中,$(‘div’) 返回了 jQuery 对象,调用 addClass() 方法后返回的还是同一个 jQuery 对象,这样就可以继续调用 css()hide() 方法。

node 事件循环机制和浏览器事件循环机制有什么区别

【JavaScript】知识点总结_第1张图片

在 Node.js 中,事件循环机制由 libuv 库实现。libuv 是一个跨平台的异步 I/O 库,提供了基于事件驱动的异步 I/O 操作和定时器功能,使 Node.js 能够快速高效地处理 I/O 操作。Node.js 事件循环机制基于 libuv 库实现,是单线程的,即所有 I/O 操作和计算操作都在同一个线程中执行。Node.js 的事件循环机制主要有以下几个阶段:timers、I/O callbacks、idle、poll、check、close callbacks。

而在浏览器中,事件循环机制由浏览器提供的 Web API 实现。常见的 Web API 包括setTimeout、setInterval、XMLHttpRequest、fetch 等。
浏览器事件循环机制与 Node.js 不同,它不是单线程的,浏览器中的 JavaScript 引擎和渲染引擎是分开的,JavaScript 引擎运行在主线程上,而渲染引擎运行在其他线程上。在事件循环机制中,浏览器将异步任务分为宏任务和微任务两类,其中微任务优先级更高,执行时机在宏任务结束之后但在渲染之前。浏览器中的微任务包括 Promise、MutationObserver 等。

Reflect

Reflect 是 ES6 引入的一个新的内置对象,提供了一组用于操作对象的方法,类似于 Object 对象上的一组方法,但是具有一些特殊的功能和用途。主要有以下作用:

  1. 对象方法的重命名
    Reflect 可以用于重命名对象的一些方法,比如将 Object.defineProperty 方法重命名为
    Reflect.defineProperty,这样就可以使用 Reflect.defineProperty 方法来操作对象属性了。
  2. 简化对象的操作
    Reflect 提供了一些方法,可以用更简单的方式操作对象。例如,Reflect.has 方法用来判断对象是否有某个属性,可以替代
    obj.hasOwnProperty(key) 或者 key in obj。
  3. 使用 Proxy 进行元编程
    Reflect 和 Proxy 是一对强力组合,可以用来进行元编程。元编程是指编写能够创建、操作和使用其他程序(包括自身)的程序。在元编程中,我们通过重载对象的行为来实现自己的需求,这样就能够编写出更加灵活、高效和安全的程序。
  4. 更加严谨的操作 Reflect 中的方法会返回 Boolean、undefined、Object
    等类型的值,这使得对象的操作更加严谨,例如使用 Reflect.defineProperty 方法来定义属性,如果属性定义成功则返回 true,否则返回 false,这样我们就可以更好的判断对象操作是否成功。

总的来说,Reflect 提供了一些优雅的、可读性高的方法,使得我们的代码更加简单和易于理解。同时,它也为我们提供了更多的元编程的能力,可以让我们更加灵活地处理对象的行为。

Object.keys和Object.getOwnPropertyNames有什么区别?

Object.keys()和Object.getOwnPropertyNames()都是用于获取对象自身属性的方法,但是它们有一些区别:

Object.keys()返回一个由对象自身可枚举属性组成的数组,不包括不可枚举属性以及继承属性;
Object.getOwnPropertyNames()返回一个由对象自身属性组成的数组,包括不可枚举属性和继承属性。
下面是一个示例代码,可以更好地理解两者之间的区别:

const obj = {
  a: 1,
  b: 2
};

Object.defineProperty(obj, 'c', {
  value: 3,
  enumerable: false
});

console.log(Object.keys(obj)); // ['a', 'b']
console.log(Object.getOwnPropertyNames(obj)); // ['a', 'b', 'c']

在这个例子中,我们使用Object.defineProperty()方法定义了一个不可枚举属性c,并将它添加到了对象obj上。当我们使用Object.keys()和Object.getOwnPropertyNames()分别获取obj的属性时,可以看到Object.keys()只返回了可枚举属性a和b,而Object.getOwnPropertyNames()则返回了所有自身属性,包括不可枚举属性c。

如何配置rem

在前端开发中,通常使用rem作为移动端适配方案,下面是一些配置rem的方法:

  1. 直接在html标签中设置字体大小:可以通过监听窗口大小改变事件,计算出新的根字体大小,然后设置到html标签上。示例代码:

    function setRootFontSize() {
      const clientWidth = document.documentElement.clientWidth;
      const rootFontSize = 100 * (clientWidth / 750); // 假设设计稿宽度为750px,设置根字体大小为100px
      document.documentElement.style.fontSize = rootFontSize + 'px';
    }
    
    // 监听窗口大小改变事件
    window.addEventListener('resize', setRootFontSize);
    
  2. 使用less或sass等预处理器:通过定义一个变量,来设置根字体大小。示例代码:

    @design-width: 750px; // 设计稿宽度
    @root-font-size: 100px; // 根字体大小
    
    html {
      font-size: @root-font-size * @screen-width / @design-width;
    }
    
  3. 使用postcss-pxtorem插件:这是一个自动将px转换为rem的插件。示例代码:

    // 安装插件
    npm install postcss-pxtorem
    
    // 在postcss配置文件中配置插件
    module.exports = {
      plugins: {
        'postcss-pxtorem': {
          rootValue: 100, // 根字体大小
         propList: ['*'] // 要转换的属性,*表示全部
       }
      }
    }
    

无论使用哪种方法,设置根字体大小的原则是:设计稿宽度除以根字体大小等于1rem的像素值,一般设置为100px,也可以根据自己的需求进行调整。

clientHeight、offsetHeight、scrollHeight有什么区别

这三个属性都是用来获取元素高度的,但是有所不同:

  1. clientHeight:表示元素的可视高度,包括 padding,但不包括 bordermargin
  2. offsetHeight:表示元素的可视高度和 border,但不包括 paddingmargin
  3. scrollHeight:表示元素的总高度,包括溢出部分的高度,需要通过滚动才能查看。

总的来说,clientHeightoffsetHeight 都是可视高度,区别在于 clientHeight 不包括边框,而 offsetHeight 包括边框。scrollHeight 则是总高度,包括溢出部分的高度。在使用的时候需要根据具体的需求来选择使用哪个属性。

BOM 和 DOM的区别

BOM(Browser Object Model)和 DOM(Document Object Model)是浏览器提供的两个核心对象模型,它们的主要区别在于:

  1. BOM是浏览器对象模型,它提供了操作浏览器窗口、浏览器的历史记录、浏览器的地址栏等一系列与浏览器相关的API,例如window、navigator、location等,而DOM是文档对象模型,它提供了操作HTML文档的API,例如document、Element、Node等。
  2. BOM提供的对象在不同浏览器之间可能存在差异,而DOM标准是由W3C制定的,浏览器对DOM的实现基本一致。
  3. BOM提供的对象可以通过全局变量window来访问,而DOM提供的对象通常是通过document对象访问。

需要注意的是,BOM和DOM并不是完全独立的两个对象模型,它们之间存在着一定的关联,例如window对象既属于BOM,也属于DOM,因为它既提供了与浏览器相关的API,也提供了访问文档的API。

倒计时用setimeout来实现还是setInterval

倒计时的实现可以使用 setInterval 或者 setTimeout,但两者存在一些区别。

使用 setInterval 实现倒计时,会以一定的时间间隔执行一次回调函数,这意味着在倒计时结束前,回调函数会被执行多次,其中可能包含不必要的操作。

而使用 setTimeout 实现倒计时,可以在每次倒计时结束后再次设置一个定时器来触发下一次倒计时,这样可以避免不必要的操作。另外,使用 setTimeout 还可以在倒计时结束时触发额外的操作,比如提示倒计时结束或者播放音效等。

因此,建议使用 setTimeout 来实现倒计时。

promise相对于async…await的优缺点

Promise 和 async/await 都是 JavaScript 中处理异步操作的方式,二者各有优缺点。

Promise 的优点包括:

  • 可以比较容易地处理多个异步操作,并控制它们的并行执行或串行执行。
  • 可以使用 .then() 方法来进行链式调用,使代码的可读性更强。
  • 可以比较容易地对异步操作的成功和失败进行处理,可以使用 .catch() 方法来捕获异步操作的错误。

Promise 的缺点包括:

  • 嵌套过深时,代码可读性会变差。
  • 必须显式地创建 Promise 实例和返回 Promise 实例,有些繁琐。

而 async/await 的优点包括:

  • 代码简洁易懂,可读性好。
  • 可以像同步代码一样处理异步操作,不需要使用回调函数或者 Promise 链。
  • 可以使用 try…catch 语句捕获异步操作的错误。

async/await 的缺点包括:

  • 需要使用 try/catch 来处理错误,可能会使代码看起来比较冗长。

  • 不支持链式调用,可能会使代码变得复杂。

因此,选择 Promise 还是 async/await,取决于具体的场景和个人偏好。

async/await 怎么抛出错误异常 ?

async/await 中,可以使用 try...catch 结构来捕获并处理异常。如果在 async 函数中抛出了异常,则该函数的返回值会被包装在一个 rejected 状态的 Promise 中,该 Promisecatch() 方法就可以捕获到这个异常。

以下是 async/await 中抛出错误异常的示例代码:

async function example() {
  try {
    // 进行一些异步操作
    const result = await someAsyncOperation();
    // 可能会抛出异常的代码块
    if (!result) {
      throw new Error('Some error');
    }
    return result;
  } catch (error) {
    console.error(error);
    // 抛出异常
    throw error;
  }
}

// 调用示例代码
example()
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error(error);
  });

在上述代码中,example() 是一个 async 函数,该函数会进行一些可能会抛出异常的异步操作。在 try 代码块中,如果执行正常且返回结果不为 nullundefined,则返回结果;否则会抛出一个错误异常。

catch 代码块中,捕捉到异常后,会将异常信息输出到控制台,然后重新抛出异常。在最后的调用示例中,使用了 catch() 方法来捕获到可能在 example() 函数中抛出的任何异常,并将它们输出到控制台。

需要注意的是,如果 throw 语句未在 try 代码块中执行,则当使用 catch 方法捕获时,将无法捕获到相应的异常。在 async/await 中,只有在 try 代码块中的异步函数或 throw 语句抛出了异常才能被 catch 方法捕获。

fetch优缺点

Fetch是一种基于Promise设计的网络请求API,它可以替代传统的XMLHttpRequest(XHR)请求,具有以下优缺点:

优点:

  1. Fetch API 使用 Promise,支持 async/await 语法,代码更加简洁易懂,可以更好地处理异步流程。
  2. Fetch API 基于标准 Promise 设计,可以完全替代
    XMLHttpRequest(XHR),支持跨域请求、CORS(跨域资源共享)。
  3. Fetch API 默认不带cookie,需要设置 credentials 属性为 “include”
    才可以携带cookie,这样可以一定程度上防止 CSRF 攻击。
  4. Fetch API 的请求和响应都是基于 stream 流的方式处理,不需要等待整个请求或响应下载完毕,提高了传输效率。

缺点:

  1. Fetch API 不支持 IE 浏览器(仅 IE11 支持部分),需要使用 polyfill 或使用 XMLHttpRequest
    进行兼容。
  2. Fetch API 没有提供超时控制,无法在规定时间内结束请求。
  3. Fetch API 对下载文件和二进制数据的支持不够完善。
  4. Fetch API 没有原生的取消请求的方法,需要通过 AbortController 实现,相对来说比较麻烦。
  5. Fetch API 的错误处理不够灵活,只能通过 .catch() 方法来捕捉异常。

总之,Fetch API 是一种更加现代、强大、灵活的网络请求API,逐渐成为前端开发中的主流工具之一,但仍需要注意其兼容性、安全性以及对错误的处理等问题。

秒传、分片传输、断点传输

秒传、分片传输和断点传输是三种常见的网络传输优化技术。

  1. 秒传:
    秒传是指当用户上传文件时,服务器先检查文件的哈希值,如果发现服务器已经存在这个文件,则无需再次上传,直接返回上传成功的结果。这样可以避免重复上传相同的文件,节省服务器的存储空间和带宽。
  2. 分片传输:
    分片传输是将大文件分割成多个小文件进行传输,每个小文件的大小根据网络环境和服务器限制而定。客户端将每个小文件上传到服务器,服务器收到所有小文件后再将它们合并成一个完整的文件。这样可以加快上传速度,减少网络阻塞和传输失败的概率。
  3. 断点传输:
    断点传输是指当文件上传过程中出现中断或失败时,客户端只需上传未上传的部分,而不需要重新上传整个文件。服务器会根据客户端上传的数据和已有的数据进行合并,最终得到完整的文件。这样可以减少因网络波动或其他因素导致上传失败时需要重新上传的时间和带宽消耗。

e.target和e.currentTarget的区别

在 JavaScript 中,事件对象通常会作为事件处理函数的第一个参数传递,通常表示为 event 或缩写为 e。事件对象提供了有关事件的详细信息,如事件类型、目标元素、鼠标位置等。

在事件处理函数中,常常会用到两个属性:e.target 和 e.currentTarget。

  • e.target 表示触发事件的元素,是事件的实际目标。当事件冒泡时,它可能与 e.currentTarget 不同。
  • e.currentTarget
    表示当前事件处理程序正在处理事件的元素,即注册事件时的目标元素。当事件冒泡时,它不会变化,始终表示注册事件时的元素。

举个例子,假设有一个 div 包含一个按钮,同时为 div 和按钮注册了 click 事件的处理程序:

//html
<div id="wrapper">
  <button id="btn">Click me!</button>
</div>
//js
const wrapper = document.querySelector('#wrapper')
const btn = document.querySelector('#btn')

wrapper.addEventListener('click', (e) => {
  console.log('currentTarget:', e.currentTarget.id)
  console.log('target:', e.target.id)
})

btn.addEventListener('click', (e) => {
  console.log('currentTarget:', e.currentTarget.id)
  console.log('target:', e.target.id)
})

当点击按钮时,事件的实际目标是按钮本身,因此 e.target 返回按钮元素。但是由于事件冒泡,该事件也会被父级元素所捕获,因此 e.currentTarget 会一直保持为注册事件时的 div 元素。

输出结果如下:

currentTarget: wrapper
target: btn
currentTarget: btn
target: btn

因此,e.target 和 e.currentTarget 的区别在于它们表示的目标元素不同,后者始终表示注册事件时的目标元素,前者则表示事件的实际目标。

call,apply,bind的用法

这三个方法都是用于绑定函数执行上下文的方法。

call和apply可以直接在函数调用时指定函数执行时所需的上下文对象(即函数内部的this值)以及参数列表。它们的区别在于,call方法可以接受参数列表,而apply方法则接受一个参数数组。

例如,这是一个使用call方法的例子:

function sayHello() {
  console.log(`Hello, ${this.name}`);
}

const person = {
  name: 'Alice'
};

sayHello.call(person); // 输出:Hello, Alice

这个例子中,我们通过调用sayHello函数时使用call方法,并传递了person对象作为上下文对象,从而确保函数内部的this值指向person对象。

而对于apply方法,我们可以将多个参数作为数组传递给函数。例如:

function add(a, b) {
  return a + b;
}

const result = add.apply(null, [2, 3]); // result = 5

在调用 apply 方法时,第一个参数可以用来指定函数执行时的上下文对象。如果该参数为 null 或 undefined,则在严格模式下函数内部的 this 值会变为全局对象。在非严格模式下, this 值也可以变成全局对象,但其行为是不可预期的,因此应该尽量避免使用。

bind方法则是将函数返回一个新函数,同时也绑定了执行函数的上下文对象,但并不会立即执行该函数。这个新函数可以先保存下来,后续需要执行时再调用。调用新函数时,绑定的执行上下文仍然是之前指定的上下文,即bind方法所传递的第一个参数。

例如:

function sayHello() {
  console.log(`Hello, ${this.name}`);
}

const person = {
  name: 'Alice'
};

const greeting = sayHello.bind(person);
greeting(); // 输出:Hello, Alice

这个例子中,我们通过调用bind方法创建了一个新函数greeting,同时将person作为上下文对象绑定了该函数。调用greeting时,函数内部的this仍然指向person对象。

~arr 是什么用法

~ 是位运算符中的非运算符(取反运算符)。它可以用来获取一个数字的位运算补码,从而达到一些比如判断数组中是否存在某个元素的目的。

如果使用 ~arr.indexOf(item),则会返回目标元素在数组中的位置(如果存在)。如果目标元素不存在,该表达式会返回 -1。但是,由于取反运算符将 -1 转换为 0,将其他非负整数转换为 -1,因此该表达式可以用于判断目标元素是否存在于数组中。

例如,以下代码可以使用 ~arr.indexOf(item) 来判断目标元素 item 是否存在于数组 arr 中:

if (~arr.indexOf(item)) {
  // 目标元素存在于数组中
}
该代码可以简化为以下形式:

if (arr.indexOf(item) >= 0) {
  // 目标元素存在于数组中
}

但是,使用 ~arr.indexOf(item) 的方式可能更为简洁、易读,因为它涉及到位运算,有一些小巧妙的技巧可以使用,但需要注意运算符的优先级和赋值方向。

load和ready区别

load和ready都是与页面加载有关的Javascript事件,但它们的触发时机略有不同。

  1. ready事件:

$(document).ready() 是 jQuery 提供的一个函数,当DOM完成解析时即可调用,而不必等待整个页面的资源都下载完毕。也就是说,当DOM树构建完毕后,就可以执行这个ready事件中的功能了。

$(document).ready(function() {
  // 在DOM结构构建完毕后执行的代码
});
  1. load事件:

load事件是在整个页面(包括图片和其他资源)都加载完成后才触发的。相比ready事件,load事件需要等待所有内容都完成载入才会执行。

$(window).on('load', function() {
  // 在页面资源都加载完毕后执行的代码
})

因此,如果我们只需要在DOM解析完毕后就执行某些功能,可以使用ready事件。而如果我们需要等待页面资源都完成载入才执行某些功能,就需要使用load事件。

for…of 和 for…in的区别

for…of 和 for…in 都是 JavaScript 中的循环语句,但它们的作用略有不同。
for…of 语句遍历可迭代对象(如 Array、Map、Set、String 等)的元素,并将每个元素赋值给指定的变量,可以看做是对迭代器的简化实现。例如:

const arr = ['a', 'b', 'c'];
for (const elem of arr) {
  console.log(elem);
}// 输出:a, b, c

for…in 语句则是遍历对象的可枚举属性,包括对象本身和其原型链上的属性,将每个属性的名称赋值给指定的变量。例如:

const obj = { a: 1, b: 2, c: 3 };
for (const prop in obj) {
  console.log(prop);
}// 输出:a, b, c

需要注意的是,for…in 遍历的是对象属性的名称,而不是属性的值,因此在使用时需要根据需要取出属性的值。此外,使用 for…in 遍历时还需要注意一些细节,例如需要使用 hasOwnProperty 方法判断一个属性是否为对象自身的属性,避免遍历到原型链上的属性。

如何实现数组的复制 ?

在 JavaScript 中,有多种方法来复制一个数组,每种方法都有其各自的优缺点。以下是常见的几种方法:

方法一:使用 Array.slice() 方法复制数组

const originalArray = [1, 2, 3, 4, 5];
const copiedArray = originalArray.slice();

方法二:使用扩展运算符(spread operator)

const originalArray = [1, 2, 3, 4, 5];
const copiedArray = [...originalArray];

方法三:使用 Array.concat() 方法将数组连接起来

const originalArray = [1, 2, 3, 4, 5];
const copiedArray = [].concat(originalArray);

方法四:使用 Array.from() 静态方法将类数组对象或可迭代对象转换为数组

const originalArray = [1, 2, 3, 4, 5];
const copiedArray = Array.from(originalArray);

方法五:使用循环逐个复制数组元素

const originalArray = [1, 2, 3, 4, 5];
let copiedArray = [];
for (let i = 0; i < originalArray.length; i++) {
  copiedArray[i] = originalArray[i];
}

需要注意的是,上述所有方法只会复制一层数组,如果原始数组中包含引用类型的元素(如对象或数组),则复制的新数组中的这些元素仍将指向原始数组中相应的引用类型对象。如果需要深度复制数组,可以考虑使用递归或第三方库等方法。

如何用一次循环找到数组中两个最大的值 ?

function findLargestTwo(arr){
  let largest = arr[0];
  let secondLargest = arr[1];
  if (secondLargest > largest){
    [largest, secondLargest] = [secondLargest, largest];
  }
  for(let i = 2; i < arr.length; i++){
    let num = arr[i];
    if (num > largest){
      secondLargest = largest;
      largest = num;
    } else if (num > secondLargest){
      secondLargest = num;
    }
  }
  return [largest, secondLargest];
}

常见的兼容问题有哪些 ?

  1. 不同浏览器之间的差异:不同的浏览器会有不同的渲染机制和对 CSS 属性的支持,导致相同的代码在不同浏览器中呈现不一致。
  2. 版本差异:同一浏览器的不同版本也可能会存在差异,特别是在早期 IE 中很常见。
  3. 不同设备的屏幕尺寸和分辨率:不同设备的屏幕尺寸和分辨率也会影响页面的渲染,需要针对不同的设备进行适配。
  4. 对 HTML5 和 CSS3 的支持:一些较旧的浏览器可能不支持最新的 HTML 和 CSS 标准,需要使用一些兼容性代码来实现相同的效果。
  5. JS API的兼容问题:不同浏览器对 JS API的支持也存在差异。HTML5的兴起,已使得很多API变得标准化,但是在某些老旧浏览器上,还是需要做兼容性适配。

常见的兼容问题的解决方法

  1. 使用 polyfillsshim:这些是一些 JavaScript 库,可以在不支持某些特性的浏览器上模拟实现它们。比如,如果浏览器不支持 HTML5 的某个新元素,你可以使用 Polyfill 来实现它的功能。
  2. 前缀属性:有些 CSS3 属性可能需要前缀才能适用于不同的浏览器,比如 -webkit-, -moz-, -o-
    等。使用这些前缀属性可以确保你的样式在不同浏览器中都能被正确显示。
  3. 弹性布局:弹性布局(Flexbox)提供了一种灵活的布局方式,可以根据不同的屏幕大小和设备类型动态地调整组件的位置和大小,从而使页面可以在各种设备上具有更好的响应性。
  4. CSS Grid:类似于弹性布局,CSS Grid 提供了一种可定制的网格布局系统,可以根据不同的元素大小和位置,将网页分成网格,从而更加灵活地布局页面。
  5. 自适应图片和媒体:图片和媒体的大小和格式都会影响网页的性能和布局。使用响应式图片和媒体的方式,可以根据不同的屏幕尺寸和设备类型加载不同的大小和格式的图片和媒体,从而提高性能和用户体验。

你了解构造函数吗 ? class 是什么 ? 两者有什么区别 ?

在 JavaScript 中,构造函数是用于创建特定类型对象的函数。它们通常以大写字母开头,使用 new 运算符进行调用,它们将返回一个对象实例。可以在构造函数中使用关键字 this 来引用要创建的对象。例如,以下是一个创建对象 Person 的构造函数:

  function Person(name, age) {
    this.name = name;
    this.age = age;
    this.getInfo = function() {
      return this.name + ' is ' + this.age + ' years old';
    }
  }

在上述代码中,Person 构造函数创建了一个对象,该对象具有属性 nameage,以及一个方法 getInfo()。现在,可以通过以下方式创建一个新的 Person 对象:

  var john = new Person('John Doe', 25);
  console.log(john.getInfo()); // 输出 "John Doe is 25 years old"

另一方面,class 是 ECMAScript 2015 中引入的语法,它不像传统的 JavaScript 中的类定义方式一样,而是基于原型的类语法糖。使用 class 可以定义一个类来创建多个对象。类定义具有构造函数,属性和方法等。例如,以下是一个用于创建 Person 类的示例:

  class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }

    getInfo() {
      return this.name + ' is ' + this.age + ' years old';
    }
  }

可以使用 new 运算符创建新对象,就像使用构造函数一样。

  var jane = new Person('Jane Doe', 29);
  console.log(jane.getInfo()); // 输出 "Jane Doe is 29 years old"

因此,两者之间的区别在于语法上的不同:class 是一种新的定义类的方式,而构造函数是一种旧的定义类的方式。但是,它们都可以用来创建对象并包括属性和方法。

在 JS 中如何阻止事件冒泡 ?

在 JavaScript 中,可以使用 stopPropagation() 方法来阻止事件冒泡。该方法可应用于任何 DOM 元素上的事件,包括 click、mouseover 和 keydown 等。

例如,以下是一个通过阻止事件冒泡来解决某些典型问题的示例:

  function onClickChild(event) {
    event.stopPropagation();
    console.log('子元素被点击了');
  }

  function onClickParent(event) {
    console.log('父元素被点击了');
  }

  var child = document.getElementById('child');
  var parent = document.getElementById('parent');

  child.addEventListener('click', onClickChild);
  parent.addEventListener('click', onClickParent);

在上述代码中,当用户单击 child 元素时,stopPropagation() 防止事件向其父级元素 parent 冒泡,并只触发 onClickChild() 函数,而不是同时触发 onClickChild()onClickParent() 函数。这种方式使其更容易理解和处理DOM中的事件操作。

JavaScript如何存储cookie

在JavaScript中可以使用document.cookie API来创建、读取和删除cookie。

以下是一些常见的操作:

  1. 创建Cookie:

通过为document.cookie设置一个键值对以及可选的cookie属性来创建一个Cookie。例如,以下是创建一个cookie并设置过期时间为7天的代码:

document.cookie = "name=value;expires=" + new Date(Date.now() + 604800000).toUTCString();
  1. 读取Cookie:

可以使用document.cookie来读取已保存的cookie。document.cookie是一个字符串,包含所有可用的cookie。

const cookies = document.cookie;
  1. 删除Cookie:

通过将cookie的过期日期设置为过去的时间来删除cookie。例如,以下代码将名为“name”的cookie从浏览器中删除:

document.cookie = "name=;expires=" + new Date(0).toUTCString();

需要注意以下几点:

  • Cookie是一种在客户端存储数据的机制,因此容易被恶意修改或删除。不要存储敏感信息在cookie中,例如用户的密码。

  • 每个域名和路径组合可容纳的cookie数量是有限制的。

  • 不能在cookie中存储大量的信息,因为每个http请求都会包含cookies,当信息过多时会影响网络性能。

symbol 你是怎么理解的 ?

Symbol(符号)它代表着一种独一无二的标识符。

可以使用 Symbol() 函数来创建一个符号,每个符号都是唯一的,即使它们具有相同的名称。符号是不可变且具有唯一性的,它们通常用于确保对象属性的唯一性,可以作为对象属性的键。

以下是 Symbol 的一些特点:

  1. 符号是一种原始数据类型,通过 Symbol() 构造函数进行创建。

  2. 符号是不可变的且唯一的,不同的符号值之间相互独立且不可修改。

  3. 符号值可以作为对象属性的键,它们保证了属性的唯一性。

以下是 Symbol 的使用示例:

const sym1 = Symbol('foo');
const sym2 = Symbol('foo');

// sym1 和 sym2 是不同的符号,因为它们使用了不同的描述字符串
console.log(sym1 === sym2); // false

const obj = {
  [sym1]: 'bar'
};

// Symbol 值作为对象属性的键
console.log(obj[sym1]); // 'bar'

// 无法使用点运算符来访问 Symbol 属性
console.log(obj.sym1); // undefined

实现一个 add 方法 使计算结果能够满足如下预期: - add(1)(2)(3)() = 6 - add(1,2,3)(4)() = 10

好的,以下是一个实现这个功能的 add 方法的示例代码:

function add() {
  let args = [];

  function innerAdd(...innerArgs) {
    args = [...args, ...innerArgs];
    return innerAdd;
  }

  innerAdd.toString = () => args.reduce((acc, curr) => acc + curr, 0);

  return innerAdd(...arguments);
}

上述代码中,add 函数使用了一个内部函数 innerAdd 来累计传入的函数参数。innerAdd 函数返回自身以支持链式调用。当调用 add 结束时,使用 toString 方法返回累计的值。

以下是两个使用 add 函数的示例:

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

希望这个代码可以帮助你实现需求。

less 和 sass 的区别 ?

Less 和 Sass 都是 CSS 预处理器,用于扩展 CSS 语言,提供了变量、嵌套、函数、混合等功能,可以简化 CSS 编写的复杂性。下面是它们的一些主要区别:

  1. 语法不同

Less 使用类似 CSS 的语法,只是在其中添加了一些扩展,如变量、嵌套等。它使用 less 后缀来命名文件。

Sass 使用自己的语法,称为 Sass/SCSS,类似于 Ruby。它使用 sass 或 scss 计数器来命名文件。在 SCSS 中,可以使用类似于 CSS 的语法,而 Sass 则有更为简洁的语法。
下面是 LESS 和 SASS 语法的示例:

/* LESS */
@main-color: #428bca;

.button {
  color: @main-color;
}
/* SASS */
$main-color: #428bca;

.button {
  color: $main-color;
}
  1. 编译方式不同

LESS 是以 JavaScript 实现的,它通过在客户端或服务器运行 less.js 来实现实时/按需编译。

Sass 是以 Ruby 实现的,需要安装 Ruby 才能运行。

  1. 活跃程度不同

Sass 的活跃开发者较多,社区也比较活跃,拥有更多的扩展和工具。

LESS 的社区相对较小,拥有的扩展和工具也相对较少。
综上所述,Less 和 Sass 在语法、编译方式和活跃程度等方面存在一些区别。不过,不论是哪种预处理器,其最终目的都是提高 CSS 编写的效率和可维护性。

对 this 的理解, 三种改变 this 的方式 ?

在 JavaScript 中,this 是一个关键字,其值取决于代码的上下文。通常情况下,this 引用当前执行代码所在的对象。

对于this的理解,可以总结为以下几点:

  1. 在全局作用域中,this 引用全局对象。
  2. 在函数作用域中,this 引用调用该函数的对象。如果函数不是作为对象的方法调用的,则 this 指向全局对象。
  3. 在箭头函数中,this 指向定义该函数的上下文环境的 this 值。箭头函数中的 this 关键字不会被动态绑定。

改变 this 的方式主要包括以下三种:

  1. 使用 call() 或 apply() 方法可以改变函数执行中的 this 值,这两个方法的第一个参数即为要设置的 this 值。
const person = {
  name: "Alice",
  age: 30,
  greet: function () {
    console.log(`Hello, my name is ${this.name}, and I am ${this.age} years old.`);
  },
};

const dog = {
  name: "Max",
  age: 5,
};

person.greet(); // 输出: Hello, my name is Alice, and I am 30 years old.

person.greet.call(dog); // 输出: Hello, my name is Max, and I am 5 years old.

person.greet.apply(dog); // 输出: Hello, my name is Max, and I am 5 years old.
  1. 使用 bind() 方法可以创建一个新函数,新函数中的 this 值被预先设置为指定的值。利用 bind()
    可以延迟函数执行,以后再使用新函数调用原函数时会应用新的 this 值。
const person = {
  name: "Alice",
  age: 30,
  greet: function () {
    console.log(`Hello, my name is ${this.name}, and I am ${this.age} years old.`);
  },
};

const dog = {
  name: "Max",
  age: 5,
};

const boundGreet = person.greet.bind(dog);

person.greet(); // 输出: Hello, my name is Alice, and I am 30 years old.

boundGreet(); // 输出: Hello, my name is Max, and I am 5 years old.
  1. 在对象方法中,使用箭头函数作为方法的值,可以确保箭头函数中的 this 值与方法所在对象的 this
    值相同。如果不使用箭头函数,则需要在方法中保存 this 的值,并在函数中使用这个值。
const person = {
  name: "Alice",
  age: 30,
  greet: function () {
    const sayHi = () => {
      console.log(`Hi, my name is ${this.name}.`);
    };
    sayHi();
  },
};

const dog = {
  name: "Max",
  age: 5,
};

person.greet(); // 输出: Hi, my name is Alice.

宏任务和微任务

在 JavaScript 中,事件循环机制(event loop)是一种用于处理异步代码的机制,它决定了代码的执行顺序。事件循环机制将任务分为两种类型:宏任务(macrotask)和微任务(microtask)。

宏任务是指由浏览器提供的任务,例如定时器事件(setTimeout、setInterval)、用户交互事件(click、input 等)和网络请求(ajax、fetch 等)等。宏任务会被放入宏任务队列中等待执行。

常见的宏任务包括:

  1. script整体代码
  2. setTimeout和setInterval
  3. I/O操作(如文件读写、网络请求等)
  4. UI渲染

微任务是指由 JavaScript 引擎提供的任务,例如 Promise 的回调函数和 MutationObserver 等。微任务会被放入微任务队列中等待执行,当宏任务队列中的所有任务执行完毕后,才会执行微任务队列中的所有任务。

常见的微任务包括:

  1. Promise的resolve或reject回调
  2. async/await中的await操作
  3. process.nextTick(Node.js环境下)

在每次事件循环中,JavaScript 引擎会从宏任务队列中取出第一个任务,执行完毕后,再从微任务队列中取出所有任务依次执行。然后再回到宏任务队列中取下一个任务,执行完毕后再执行微任务队列中的所有任务,如此循环往复,直到所有任务执行完毕。

JS性能优化的方式

  1. 优化页面加载速度,减少 HTTP 请求次数,压缩和合并 JS 文件。

  2. 压缩文件:使用 Gzip 压缩、图片压缩等。

  3. 避免全局变量,使用模块化开发,减少命名冲突。

  4. 将脚本放在页面底部或者使用 defer,让页面尽可能快地呈现出来。

  5. 尽量使用事件委托,减少事件绑定次数。

  6. 使用 Web Worker 来处理一些耗时的任务,避免阻塞主线程。

  7. 避免频繁的请求后端接口,尽可能使用缓存和本地存储。

  8. 使用缓存:设置合适的缓存头、使用浏览器缓存、使用 CDN 缓存等。

  9. 使用 CDN 加速静态资源的加载。

  10. 避免使用 eval:eval 函数可以执行任意 JavaScript 代码,但是它会破坏作用域链,不安全且性能低下。尽量避免使用 eval 函数。

  11. 使用 requestAnimationFrame:requestAnimationFrame 是一种更加高效的动画效果实现方式,比 setInterval 更加节省性能。

  12. 减少重绘和回流:重绘和回流是非常消耗性能的操作。尽量避免频繁的样式修改,尽量将样式修改集中在一起,减少 DOM 操作。

  13. 使用懒加载,只在需要时加载资源,减少首屏加载量。

你可能感兴趣的:(javascript,前端,开发语言)