js 知识点

94 阅读21分钟

JS基础

执行上下文

执行上下文是指代码在执行期间运行的环境。每个上下文都有一个关联的对象,而这个上下文中定义的所有变量和函数都存在这个对象上。上下文在其所有代码执行完毕后会被销毁。

  • 全局执行上下文
    一般是window对象

  • 函数执行上下文
    当调用该函数时,就会形成一个函数执行上下文

作用域与作用域链

作用域:
定义当前执行代码对变量访问权限的一套规则。

  • 全局作用域: 在全局作用域中声明的变量可以在程序的任何地方访问。在浏览器环境中,全局变量通常附加到window对象上。
  • 函数作用域: 在函数内部声明的变量仅在函数内部可见,这些变量被称为局部变量。这些变量不能在函数外部访问,但可以访问到全局变量。
  • 块级作用域: {}

作用域链:
当一个变量在当前作用域中未被找到时,JS引擎会沿着作用域链查找该变量,直到找到或达到全局作用域。

闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包优点:

  • 创建全局私有变量,避免变量全局污染
  • 可以实现封装、缓存等

闭包缺点:

  • 创建的变量不能被回收,容易消耗内存,使用不当会导致内存溢出
  • 解决:  在不需要使用的时候把变量设为null

使用场景:

  • 用于创建全局私有变量
  • 封装类和模块
  • 实现函数柯里化

事件循环(event loop)

事件循环指的是js代码所在运行环境(浏览器、nodejs)编译器的一种解析执行规则。

宏任务、微任务介绍:

  1. js代码主要分为两大类:同步代码、异步代码
  2. 异步代码分为:宏任务、微任务
  3. 宏任务: script定时器网络请求
  4. 微任务: Promise

事件循环执行机制:

  1. 所有的同步任务都在主线程上执行,形成一个执行栈
  2. 遇到异步任务, 进入异步处理模块并注册回调函数; 等到指定的事件完成(如ajax请求响应返回, setTimeout延迟到指定时间)时,异步处理模块会将这个回调函数移入异步任务队列。
  3. 当栈中的代码执行完毕,执行栈中的任务为空时,主线程会先检查微任务队列中是否有任务,如果有,就将微任务队列中的所有任务依次执行,直到微任务队列为空; 之后再检查宏任务队列中是否有任务,如果有,则取出第一个宏任务加入到执行栈中,之后再清空执行栈,检查微任务,以此循环,直到全部的任务都执行完成。

箭头函数和普通函数的区别

  • 箭头函数是匿名函数,不能作为构造函数,不能使用new关键字。
  • 箭头函数没有自己的this,会获取所在的上下文作为自己的this
  • callapplaybind方法不能改变箭头函数中的this指向

call、apply、bind

  • 都可以用作改变this指向
  • callapply的区别在于传参,callbind都是传入对象。apply传入一个数组。
  • callapply改变this指向后会立即执行函数,bind在改变this后返回一个函数,不会立即执行函数,需要手动调用。

连续多个 bind,最后this指向是什么?
在 JavaScript 中,连续多次调用 bind 方法,最终函数的 this 上下文是由第一次调用 bind 方法的参数决定的

const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
const obj3 = { name: 'obj3' };

function getName() {
  console.log(this.name);
}

const fn1 = getName.bind(obj1).bind(obj2).bind(obj3);
// 输出 "obj1"
fn1(); 

原型和原型链

- 原型

js通过构造函数来创建对象,每个构造函数内部都会一个原型prototype属性,它指向另外一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。

  • proto: 当使用构造函数创建一个实例对象后,可以通过__proto__访问到prototype属性。
  • constructor:实例对象通过这个属性可以访问到构造函数

- 原型链

每个实例对象都有一个__proto__属性指向它的构造函数的原型对象,而这个原型对象也会有自己的原型对象,一层一层向上,直到顶级原型对象null,这样就形成了一个原型链。
当访问对象的一个属性或方法时,当对象身上不存在该属性方法时,就会沿着原型链向上查找,直到查找到该属性方法位置。
注意: 原型链的顶层原型是Object.prototype,如果这里没有就只指向null

构造器和操作符 "new"

构造函数在技术上是常规函数。不过有两个约定:

  1. 它们的命名以大写字母开头。
  2. 它们只能由 "new" 操作符来执行。

new 关键字会进行如下的操作:

  1. 创建一个新的空对象。
  2. 新对象的 __proto__属性(原型) 被设置为构造函数的 prototype属性 。小解:新对象继承了构造函数的原型对象上的所有属性和方法。
  3. 构造函数的this指向这个新对象,执行构造函数中的代码。在构造函数内部,可以通过this关键字引用新对象,并可以在新对象上设置属性和方法。
  4. 如果构造函数没有明确返回其他对象,那么new操作符默认返回新创建的对象实例。这个新对象现在包含了在构造函数中定义的属性和方法,并且继承了构造函数原型上的属性和方法。

事件委托

事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。

优点:

  • document 对象随时可用,任何时候都可以给他添加事件处理程序。
  • 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省dom引用,也可以节省时间。
  • 节省整个页面所需的内存,提升整体性能

函数柯里化

柯里化是把接收 多个参数 的函数变换成接收一个 单一参数(最初函数的第一个参数) 的函数,并且返回接收余下参数且返回结果的新函数的技术。

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

Promise

Promise是异步编程的一种解决方案,将异步操作以同步操作的流程表达出来,避免了地狱回调。

- Promise实例有三个状态

  • Pending(初始状态)
  • Fulfilled(成功状态)
  • Rejected(失败状态)

- Promise实例有两个过程

  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejectedRejected(已拒绝)
    注意:一旦从进行状态变成为其他状态就永远不能更改状态了,其过程是不可逆的。

- Promise构造函数接收一个带有resolvereject参数的回调函数

  • resolve的作用是将Promise状态从pending变为fulfilled,在异步操作成功时调用,并将异步结果返回,作为参数传递出去
  • reject的作用是将Promise状态从pending变为rejected,在异步操作失败后,将异步操作错误的结果,作为参数传递出去。

- Promise的缺点

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

- Promise的方法

  • promise.then() 对应resolve成功的处理。
  • promise.catch()对应reject失败的处理。
  • promise.all()可以完成并行任务,将多个Promise实例数组,包装成一个新的Promise实例,返回的实例就是普通的Promise。有一个失败,代表该Primise失败。当所有的子Promise完成,返回值时全部值的数组
  • promise.race()类似promise.all(),区别在于有任意一个完成就算完成
  • promise.allSettled() 返回一个在所有给定的 promise 都已经 fulfilledrejected 后的 promise ,并带有一个对象数组,每个对象表示对应的promise 结果。

对async/await 的理解

async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。通过async关键字声明一个异步函数, await 用于等待一个异步方法执行完成,并且会阻塞执行async 函数返回的是一个 Promise 对象,如果在函数中 return 一个变量,async 会把这个直接通过 Promise.resolve() 封装成 Promise 对象。如果没有返回值,返回 Promise.resolve(undefined)

async/await和Promise的关系

  • async/await是为优化then链而开发出来的
  • 执行 async 函数,返回的一定是 Promise 对象
  • await 相当于 Promise 的 then
  • try...catch 可捕获异常,代替了 Promise 的 catch

0.1+0.2为什么不等于0.3?

因为浮点数运算的精度问题。在计算机运行过程中,需要将数据转化成二进制,然后再进行计算。 因为浮点数自身小数位数的限制而截断的二进制在转化为十进制,就变成0.30000000000000004,所以在计算时会产生误差。

解决方案
通过去除小数点,将数字转换为整数,然后计算和,最后再将结果除以 10 的幂,该幂对应于原始数字中最大小数位数。这样可以最大程度上保持计算的精度。

function preciseAdd(num1, num2) {
    // 获取小数点后的长度
    let n1Len = num1.toString().split(".")[1]?.length || 0;
    let n2Len = num2.toString().split(".")[1]?.length || 0;
    // 计算最大需要乘以的值
    let n = Math.pow(10, Math.max(n1Len, n2Len));
    let sum = n * num1 + n * num2;

    return sum / n;
}

// 输出是 0.3
preciseAdd(0.1, 0.2)

类数组对象,如何转换为数组?

类数组也叫伪数组,类数组和数组类似,但不能调用数组方法,常见的类数组有arguments、通过document.getElements获取到的内容等,这些类数组具有length属性。

解决方案一
通过 call 调用数组的 slice 方法来实现转换

Array.prototype.slice.call(arrayLike)

解决方案二
通过 Array.from 方法来实现转换

Array.from(arrayLike)

尾调用,使用尾调用有什么好处?

尾调用就是在函数的最后一步调用函数,在一个函数里调用另外一个函数会保留当前执行的上下文,如果在函数尾部调用,因为已经是函数最后一步,所以这时可以不用保留当前的执行上下文,从而节省内存。但是ES6的尾调用只能在严格模式下开启,正常模式是无效的。

深拷贝和浅拷贝

浅拷贝: 其属性与拷贝源对象的属性共享相同引用(指向相同的底层值)的副本。因此,当你更改源或副本时,也可能导致其他对象也发生更改

  • Object.assign()
  • 展开运算符 ...

深拷贝:完全拷贝一个新对象,修改时原对象不再受到任何影响

  • JSON.parse(JSON.stringify(obj))
    缺点:当值为函数、undefined、或symbol时,无法拷贝

  • 递归逐一赋值

    function clone(obj) {
      const newObj = {};
    
      if (obj === null) return null;
      if (typeof obj !== "object") return obj;
      if (obj.constructor === Date) return obj;
      if (obj.constructor === RegExp) return obj;
    
      for (let key in obj) {
        const val = obj[key];
    
        // 不遍历原型链上的属性
        if (obj.hasOwnProperty(key)) {
          newObj[key] = typeof val === "object" ? clone(val) : val;
        }
      }
    
      return newObj;
    }
    
    

对ajax的理解,实现一个ajax

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。 创建AJAX请求的步骤:

  1. 创建一个 XMLHttpRequest 对象。
  2. 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  3. 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  4. 当对象的属性和监听函数设置完成后,最后调用 send 方法来向服务器发起请求,可以传入参数作为发送的数据体
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", '/url', true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

ajax、axios、fetch的区别

ajax

  • 基于原生XHR开发,XHR本身架构不清晰。
  • 多个请求之间如果有先后关系的话,就会出现回调地狱。
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

axios

  • 支持PromiseAPI
  • 从浏览器中创建XMLHttpRequest
  • 从 node.js 创建 http 请求
  • 支持请求拦截和响应拦截
  • 自动转换JSON数据
  • 客服端支持防止CSRF/XSRF

fetch

  • 浏览器原生实现的请求方式,ajax的替代品。
  • 基于标准 Promise 实现,支持async/await
  • fetchtch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理。
  • 默认不会带cookie,需要添加配置项。
  • fetch没有办法原生监测请求的进度,而XHR可以。

如何判断一个对象是空对象

Object.keys()  静态方法返回一个由给定对象自身的可枚举的字符串键属性名组成的数组。

  • Object.keys(obj).length === 0
  • JSON.stringify(obj) === '{}'


手写函数

函数节流

事件触发后,规定时间内,函数不能被再次调用。指连续触发事件但是在 n 秒中只执行一次函数。

使用场景:滚动加载更多、高频点击

function throttle(fn, wait = 400) {
    let last = 0;
    return function () {
      // +new Date() 获取时间戳
      const current_time = +new Date();
      // 判断当前时间减去上次结束时间是否大于设置的间隔时间
      if (current_time - last > wait) {
        fn.apply(this, arguments);
        last = +new Date();
      }
    };
}

函数防抖

多次触发,事件处理函数只能执行一次,并且是在触发操作结束时执行。

使用场景: 窗口大小resize变化后,再重新渲染;搜索框搜索输入,并在输入完以后自动搜索。

function debounce(fn, wait=400) {
    let timer;
    
    return function () {
      clearTimeout(timer);
      timer = setTimeout(function () {
        fn.apply(this, arguments);
      }, wait);
    };
}

手写 new

  function myNew(context) {
    // 创建一个新的空对象
    const obj = new Object();
    // 设置空对象的 `__proto__` 为构造函数的 `prototype`
    obj.__proto__ = context.prototype;
    // 构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
    const res = context.apply(obj, [...arguments].slice(1));
    // 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象
    return typeof res === "object" ? res : obj;
  }

手写 call

call 方法在 JS 中用于改变函数的 this 指向,同时还可以传递参数进行函数调用。

  Function.prototype.myCall = function (context = window, ...arg) {
    context.fn = this;
    const result = context.fn(...arg);
    delete context.fn;

    return result;
  };

手写 instanceof

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

思路:

  1. 通过 Object.getPrototypeOf 获取对象的原型
  2. 循环判断 objProtoType 获取到的原型对象
    1. 如果为空就返回 false
    2. objProtoTypeconstructor 的原型相等就返回 true
    3. 如果不相等,就获取 objProtoType 的原型,并给该变量重新赋值,进入下循环
  function myInstanceof(obj, type) {
    let objPrototype = Object.getPrototypeOf(obj);

    while (true) {
      if (!objPrototype) return false;
      if (objPrototype === type.prototype) return true;

      objPrototype = Object.getPrototypeOf(objPrototype);
    }
  }

字符串反转

Array.from("123456").reduce((pre, cur) => `${cur}${pre}`);

列表转树结构

  function arrTotree(arr) {
    const map = {},
      result = [];

    for (let item of arr) {
      map[item.id] = item;
    }

    for (let i = 0; i < arr.length; i++) {
      const pid = arr[i].pid;
      // 如果在map中查询到父级id,就不为根元素
      if (map[pid]) {
        // 下面两步用了引用类型的特性
        map[pid].children = map[pid].children || [];
        map[pid].children.push(arr[i]);
      } else {
        result.push(arr[i]);
      }
    }

    return result;
  }

多维数组扁平化

思路: 使用递归。循环判断当前项是否为数组,如果是就递归调用,不是就push到新数组。

  function flatten(arr) {
    let result = [];

    for (let i = 0; i < arr.length; i++) {
      const item = arr[i];
      Array.isArray(item) ? (result = result.concat(flatten(item))) : result.push(item);
    }

    return result;
  }

数组去重

- 方案一 使用Set

const a = [1, 2, 3],
 b = [4, 5, 6, 1, 3];

const arr = [...new Set([...a, ...b])];

冒泡排序

- 方案一 使用sort

[10, 9, 3, 2, 1, 4, 5].sort((a, b) => a - b)

- 方案二 双循环

思路:

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素会是最大的数
  const arr = [10, 9, 3, 2, 1, 4, 5];

  function bubbleSort(arr) {
    const len = arr.length;

    for (let i = 0; i < len; i++) {
      for (j = 0; j < len - 1; j++) {
        // 相邻的元素对比
        if (arr[j] > arr[j + 1]) {
          const first = arr[j];
          arr[j] = arr[j + 1];
          arr[j + 1] = first;
        }
      }
    }

    return arr;
  }

  bubbleSort(arr);


浏览器与网络

浏览器中的垃圾回收机制

JS 代码运行时,需要分配内存空间来储存变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间。如果不及时清理,会造成系统卡顿、内存溢出,这就是垃圾回收。

哪些情况会导致内存泄漏

  • 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  • 被遗忘的计时器或回调函数:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • 脱离 DOM 的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
  • 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。

输入url到展示的过程

  1. DNS解析‌:当用户在浏览器中输入一个域名(如www.example.com)时,浏览器首先会进行DNS解析,将域名转换为对应的IP地址。这一过程包括检查本地DNS缓存、hosts文件,如果找不到则向DNS服务器发送请求以获取IP地址。
  2. 建立TCP连接‌:通过DNS解析得到的IP地址,浏览器与服务器建立TCP连接。这一过程通过三次握手完成,确保双方都能正常通信。
  3. 发送HTTP请求‌:TCP连接建立后,浏览器向服务器发送HTTP请求,包含请求方法(如GET、POST)、请求头部(如User-Agent、Cookie)和请求体(对于POST请求)。
  4. 服务器处理请求‌:服务器接收到请求后,进行相应的处理,可能包括读取数据库、处理业务逻辑、生成动态内容等。
  5. 返回HTTP响应‌:服务器将处理结果封装成HTTP响应,包括状态码、响应头部和响应体。常见的状态码有200表示成功,404表示资源未找到,500表示服务器内部错误等。
  6. 下载页面资源‌:浏览器收到响应后,解析响应头部和响应体。如果响应体是HTML文档,浏览器会继续下载其中引用的其他资源,如CSS文件、JavaScript文件、图片等。
  7. 解析和渲染页面‌:浏览器使用HTML解析器将HTML文档解析成DOM树,使用CSS解析器将CSS文件解析成样式规则。然后,根据DOM树和样式规则进行渲染,将页面内容显示在屏幕上。如果页面包含JavaScript代码,浏览器会执行这些代码,可能修改DOM树、处理用户交互、发送异步请求等。

浏览器中的回流和重绘

在讨论回流与重绘之前,我们要知道:

  1. 浏览器使用流式布局模型 (Flow Based Layout)。
  2. 浏览器会把HTML解析成DOM,把CSS解析成CSSOMDOMCSSOM合并就产生了Render Tree
  3. 有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
  4. 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一
    注意:回流必将引起重绘,重绘不一定会引起回流。回流比重绘的代价要更高。

回流

Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。 会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见DOM元素
  • 激活CSS伪类(例如::hover
  • 查询某些属性或调用某些方法

重绘

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

如何优化

  1. css方面
  • 避免使用table布局。
  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolutefixed的元素上。
  • 避免使用CSS表达式(例如:calc())。
  1. js方面
  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流

http与https的区别

安全性
HTTP和HTTPS是两种不同的协议,它们之间最主要的区别在于安全性。HTTP协议以明文方式发送内容,不提供任何方式的数据加密,容易被攻击者截取信息。
HTTPS则在TCP和HTTP网络层之间加入了SSL/TLS安全协议,使得报文能够加密传输,保证了数据的安全性。

端口号
HTTP和HTTPS使用的是完全不同的连接方式用的端口也不一样,HTTP是80、HTTPS是443。

证书
HTTPS需要申请证书,HTTP不需要。



设计模式

你对设计模式有什么了解

  • 单例模式:保证类只有一个实例,并提供一个访问它的全局访问点。
  • 工厂模式:用来创建对象,根据不同的参数返回不同的对象实例。
  • 策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
  • 装饰器模式:在不改变对象原型的基础上,对其进行包装扩展。
  • 观察者模式:定义了对象间一种一对多关系,当目标对象状态发生改变时,所有依赖它对对象都会得到通知。
  • 发布订阅模式: 基于一个主题/事件通道,希望接收通知的对象通过自定义事件订阅主题,被激活事件的对象(通过发布主题事件的方式被通知)。