被面试官嫌弃基础不扎实,那你这些js知识一定要补补

387 阅读13分钟

深浅拷贝

const arr1 = [1,2]
const arr2 = arr1;
arr2[1] = 3;
arr1//[1,3]
arr2//[1,3]

对引用类型进行赋值,发现一旦一方改变,另一方也会改变,之所以会产生这个问题,就是因为等号赋值只能赋值引用类型的地址。于是出现浅拷贝,那如何实现,方法就太多了,至少要记住三个

数组

arr2 = [...arr1]
arr2 = arr1.concat()
arr2 = arr1.slice()
arr2 = Object.assign([],arr1)
arr2 = Array.from(arr1)
arr2 = arr1.map(item=>item)

对象

obj2 = {...obj1}
obj2 = Object.assign({},obj1)

for in

obj2 = cloneShalow(obj1)
function cloneShalow(obj){
    const res=Array.isArray(obj)?[]:{}
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            res[key]=obj[key]
        }
    }
    return res
}

但是如果item是引用类型,那么浅拷贝就解决不了了,这时候就得用深拷贝

方法一:JSON.parse(JSON.stringify(obj)) const b=JSON.parse(JSON.stringify(a)) 缺陷: 会忽略 undefined 会忽略 symbol 不能序列化函数 不能解决循环引用的对象 方法二:MessageChannel

function structralClone(obj) {
  return new Promise((resolve) => {
    const { port1, port2 } = new MessageChannel();
    port2.onmessage = (ev) => resolve(ev.data);
    port1.postMessage(obj);
  });
}
var a = { b: new RegExp(/[1-9]/) };
// a.self = a;
(async () => {
  const b = await structralClone(a);
  console.log(b);
})();

缺点:

  • 对性能有影响
  • 拷贝的对象有函数会报错
  • 日期会变成字符串 优点:
  • 能解决循环引用 方法三:lodash 的深拷贝函数(性能最好,但处理不了 WeakMap 和 WeakSet,由于库的设定,拷贝的对象嵌套不能超过四层) 方法四:自定义深拷贝函数(常考手写)
function cloneDeep(source, map = new WeakMap()) {
  if (map.get(source)) {
    return map.get(source);
  }
  //基础数据类型
  if (typeof source !== "object" || source == null) {
    return source;
  }
  //特殊对象
  const types = [Date, Map, Set, RegExp];
  if (types.includes(source.constructor)) return new source.constructor(source);

  const res = new source.constructor();
  map.set(source, res);

  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      res[key] = cloneDeep(source[key], map);
    }
  }

  //处理symbol作为对象属性的情况
  const symbolKeys = Object.getOwnPropertySymbols(source);
  for (const symKey of symbolKeys) {
    res[Symbol(symKey.description)] = cloneDeep(source[symKey]);
  }
  return res;
}

具体过程可以看我另一片文章手写深拷贝,你写全了吗 方法五 structuredClone

  • 浏览器兼容性问题
  • function、WeakMap、WeakSet、Symbol 会报错

原型

function Person(){}
const p = new Person();

隐示原型 __proto__ 显示原型prototype

每个函数都有一个显示原型 Person.prototype 每个实例都有一个隐性原型 p.__proto__

每个实例的隐示原型指向创造改对象的构造函数的原型

p.__proto__===Person.prototype

类的原型的构造函数指向自己

Person.prototype.constructor=Person
p.__proto__.constructor=Person

构造函数的prototype指向显示原型

Person.prototype=(Person.prototype)

原型链

prototype

理解原型链图的各个难点

  • Function.prototype 和 Object.prototype 是两个特殊的对象,他们由引擎来创建
  • 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
  • 对象是通过函数创造的,任何引用类型都是对象,包括函数
  • 所有原型对象都是Object的实例,除了Object自己的原型对象,因为Object的原型对象的__proto__为null
  • 任何构造函数都是Function创造的
  • Function.prototype.proto === Object.prototype是为了兼容之前的代码

instanceOf

判断是否在原型链上 var person = new Person(); person instanceOf Person//true [] instanceOf Array//true

手写instanceOf

function instanceOf(left,right){
  const proto = left.__proto__;
  const prototype = right.prototype;
  while(true){
    if(proto===prototype){
      return true;
    }
    if(proto==null){
      return false;
    }
    proto=proto.__proto__;
  }
}

new

new 的执行过程

  1. 创建一个新对象
  2. 把新对象的隐示原型和构造函数的显示原型做链接
  3. 执行构造函数的代码,给新对象添加属性和方法
  4. 返回新对象
```js
function createObj(Father, ...arg){
  const son = {};
  son.__proto__ = Father.prototype
  const result = Father.apply(son, arg)//Father是构造函数
  return (result && typeof result === 'object')? result : son;
}

作用域

定义:根据名称来查找变量的一套规则,可以把作用域通俗理解为一个封闭的空间,这个空间是封闭的,不会对外部产生影响,外部空间不能访问内部空间,但是内部空间可以访问将其包裹在内的外部空间。 作用:隔离变量 作用域有哪些

词法作用域是编译时就已经确定,而动态作用域是调用才能确定。

let a  = '111'
function sayA(){
    console.log(a);
}
function say(){
    let a = '222'
    sayA()
}
say()  //输出111

由于sayA在全局里面,当a在sayA找不到时,就在全局找,词法作用域得看当时定义所处的位置,而不是谁调用。this(动态作用域)才会看调用方。

let a  = '111'
function say(){
    let a = '222'
    sayA()
    function sayA(){
      console.log(a);
    }
}
say()  //输出222

由于sayA在say里面,当a在sayA找不到时,就在say找

自由变量:一个变量在当前没有定义,但被使用了。于是就会一层一层往上找,如果在全局也没有,就会报错。上面的a就是自由变量。

全局作用域:全局作用域是直接写在script标签中的JS代码,或者单独的JS文件中的作用域。浏览器全局作用域是window,node全局作用域是global.globalThis=window||global。 函数级作用域:每个函数内部都是一个作用域,内部函数可以访问外部函数的变量,外部函数则不能访问内部函数的变量。 块级作用域:es6出现let、const才有的块级作用域,在let所在的{}里,会形成暂存性死区,必须得先定义后使用,且变量不能重复定义

{
  console.log(a)//报错
  let a;
}

作用域链:作用域的集合

var a=1;//全局作用域
function fn(){//函数级作用域
  b=2;
  {
    let c=3;
      console.log(a,b,c)//块级作用域:1 2 3
  }
  var b;
}
fn();

动态作用域:this

this

执行上下文

this 指向

  • 简单调用时,this 指向全局
  • 严格模式时,this 指向 undefined
  • 对象调用方法时,方法的 this 指向调用的对象
  • 使用 call、apply、bind 时,this 指向指定的对象
  • 使用 new 关键字时,this 指向创建的对象
  • 必包,this 指向全局
  • new>call|apply|bind>调用

如何改变 this 指向

手写call、apply、bind

call

function call(context,...args){
    const _context = context || globalThis;
    const fn = Symbol('fn')
    Object.definePropty(_context,fn,{
        enumrable: false,
        value: this,
    )
    delete _context.fn;
    const res = _context.fn(...args);
    return res;
}

apply

function apply(context,args){
    const _context = context || globalThis;
    const fn = Symbol('fn')
    Object.definePropty(_context,fn,{
        enumrable: false,
        value: this,
    )
    delete _context.fn;
    const res = _context.fn(...args);
    return res;
}

bind

function bind(context,...args){
    const _context = context == null ? globalThis : Object(context);
    const fn = Symbol('fn')
    const that = this;
    return function F(...innerArgs){
        return that.apply(this instanceof F ? that : _context,[...args,...innerArgs])
    }
    
}

箭头函数

箭头函数特点

  • 函数体内this是定义时所在的对象
const fn = ()=>{//定义在全局
  console.log(this)//非严格模式指向全局,严格模式指向undefined
}
fn();
function fn1(){
  setTimeout(()=>{//定义在fn1
    console.log(this.a)//1
  })
}
fn1.call({a:1})//fn1.a=1
class Person{
  constructor(){
    this.name='lwp'
  }
  getName=()=>{//定义在构造函数里
    console.log(this.name)
  }
}
const p = new Person();
const {getName} = p
getName()//lwp

//class Person相当于
function Person(){
  this.name='lwp'
  this.getName=()=>{//定义在构造函数里
    console.log(this.name)
  }
}
  • 必须先定义后使用
  • 没有arguments、caller、callee
  • 无法用call、apply、bind改变this
  • 没有原型属性
  • 不能作为generator函数,不能使用yield关键字
const fn=()=>{
  return {
    name:'lwp'
  }
}
console.log(fn())

不适合用箭头函数的场景

1.对象方法

const obj={
  name:'lwp',
  getName:()=>{
    console.log(this.name)
  }
}

2.作为原型

function Person(){
  this.name='lwp'
}
Person.prototype.getName=()=>{
  console.log(this.name)
}

3.作为构造函数

const Person = () =>{
  this.name = 'lwp'
}
const p = new Person();

4.动态上下文中的回调函数

const btn = document.getElementById('btn');
btn.addEventListener('click',()=>{
  this.innerHTML='clicked'//原本想改变btn文本
})
  1. Vue生命周期和method(本质methods等函数挂在一个对象里)
{
  data(){return {name:'lwp'}},
  methods:{
    getName:()=>{
      return this.name
    }
  },
  mounted:()=>{
    console.log(this.name)
  }
}

必包

闭包是指有权访问另一个函数作用域中的变量的函数,通常是在嵌套函数中实现。

function fn1() {
  var a = 1;
  return function fn2() {
    console.log(a++);
  };
}
var fn = fn1();
fn(); //1
fn(); //2

必包的设计解决了什么问题

动态作用域难以模块化的问题

必包优缺点

优点

  • 保护私有变量,避免全局污染
  • 延长变量生命周期
  • 实现模块化
  • 保持状态

缺点

  • 内存占用
  • 性能损耗:涉及到作用域链查找过程

必包怎么回收

如果必包引用的函数是全局变量,那么必包会一直存在到页面关闭,如果这个必包以后不在使用的话,就会造成内存泄露 如果这个必包引用的是局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断必包这块内容如果不再被使用了,那么 Javascript 引擎垃圾回收器就会回收这块内存

解决必包的内存泄漏问题

function fn1(){
    var a;
    return fn2(){
        console.log(a++);
    }
}
var fn = fn1();
fn();
//销毁fn
fn=null;

应用场景

  • for 循环
  • 封装私有变量和函数
  • 模块化实现
  • 实现高阶函数
  • 实现回调函数
  • 模拟块级作用域
  • React Hook

for 循环

for (var i = 0; i < 9; i++) {
  (function (i) {
    console.log(i);
  })(i);
}

封装私有变量和函数

function createPerson() {
  var _name;
  return {
    getName: function () {
      return _name;
    },
    setName: function (name) {
      _name = name;
    },
  };
}
var lwp = createPerson();
lwp.setName("兰为鹏");
console.log(lwp.getName()); //兰为鹏

模块化实现

(function () {
  function add(a, b) {
    return a + b;
  }
  function sub(a, b) {
    return a - b;
  }
  globalThis.countFn = {
    add: add,
    sub: sub,
  };
})();
var sum = countFn.add(1, 2);
console.log(sum);

实现高阶函数

function addBy(a) {
  return function (b) {
    return a + b;
  };
}
var addTwo = addBy(2);
console.log(addTwo(5));

实现回调函数

function getNameAync(cb) {
  setTimeout(() => {
    cb && cb("lwp");
  }, 2000);
}
getNameAync((name) => {
  console.log(name);
});

React Hook

hook实现.png

事件循环

事件循环又叫消息循环,是浏览器渲染主线程的工作方式。在chorome中,它开启一个不会结束的循环,每次都循环从消息队列中取出第一个任务执行,而其他线程只需要在适合的时候将任务加入到队列末尾即可。 过去把消息队列简单分为宏任务和微任务(微任务比宏任务先执行),这种说法目前已经无法满足复杂的浏览器环境,取而代之是一种更加灵活多变的处理方式。根据whatwg最新文档的解释,每个任务又不同的类型,同类型必须放在一个队列,不同任务可以属于不同的队列。不同的任务队列又不同的优先级,在一次的事件循环中,由浏览器自行绝对取哪一个队列的任务,但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。

whatwg-cn.github.io/html/multip… 8.1.6事件循环

宏任务

script setTimeout setInterval I/O UI Render setImmediate(node)

  • MessageChannel
const channel = new MessageChannel();
const massage = {data:'lwp'}
channel.port1.postMessage(massage);
channel.port2.onmessage=function(event){
  console.log(event.data)
}
setTimeout(()=>{
  console.log('set')
})

微任务

promise.then process.nextTick(node) Object.observer (已废弃;Proxy 对象替代)MutationObserver async await

  • nextTick
setTimeout(()=>{
  console.log('setT')
})
nextTick(()=>{
  console.log('nextTick')
})
//nextTick
// setT
  • proxy
let person={
  name:'lwp'
}
let p1 = new Proxy(person,{
  set(){
    console.log('set')
  }
})
setTimeout(()=>{
  console.log('setT')
})
p1.name='hw'
//set
//setT
  • MutationObserver
 <div id="root"></div>
  <script>
    const root = document.getElementById('root');
    setTimeout(() => {
     console.log('setT') 
    });
    let observer = new MutationObserver(()=>{
      console.log('dom change')
    })
    observer.observe(root,{
      childList:true,
   
    })
    const a = document.createElement('a');
    a.innerHTML="hh"
    root.appendChild(a)
   
  </script>

综合面试题

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
console.log("js start");
setTimeout(function () {
  console.log("timeout");
}, 0);
async1();
new Promise(function (resolve) {
  console.log("promise");
  resolve();
}).then(function () {
  console.log("then");
});
console.log("js end");
  • 首先执行整段代码当成宏任务,从上往下依次执行
  • 第一步:输出js start
  • 遇到setTimeout,放入到宏任务
  • 执行async1
  • 输出async1 start
  • await async2,线程到这里阻塞,必须等到async2执行完毕
  • 于是输出async2
  • async1 end被当成微任务放到队列,因为aysnc的底层是promise["async1 end"]
  • 执行newPromise立即执行函数
  • 于是输出promise
  • then放到微任务["async1 end","then"]
  • 输出js end
  • 把微任务队列拿出来放到主线程去执行
  • 输出async1 end
  • 输出then
  • 把宏任务队列放到主线程
  • 输出timeout
  • 以下就是完整的执行结果
//js start
//async1 start
//async2
//promise
//js end
//async1 end
//then
//timeout

模块化

ES-Module

// a.js
export function a (){}
export default A={}
// b.js
import { a } from './a'
import A from './a'

CJS和AMD都是运行时确定

优点:容易静态分析 缺点:原生浏览器还没实现该标准

CommonJs

NodeJs独有实现

//a.js
module.exports={
  a:1
}
exports.b=1
//b.js
const A = require('a')
const {b} = requrie('a')
A.a

缺点

  • 同步的加载方式不适合在浏览器环境中
  • 不能并行加载多个模块 AMD RequireJs提出
define(['getSum'],function(math){
  return function(a,b){
    return a+b
  }
})
require(['getSum'],function(getSum){
  getSum(2,3)
})

优点:

  • 适合在浏览器环境中异步加载模块
  • 可以并行加载多个模块 缺点:
  • 提高了开发成本
  • 不符合通用的模块化思维方式 UMD 兼容AMD和CJS:如果支持node,就使用CJS,再判断是否支持AMD,支持就使用AMD
(function(window, factory){
  if(typeof exports ==="object"){
    //CJS
    module.exports=factory();
  } else if(typeof define==='function'&&define.amd){
    //AMD
    define(factory)
  }else{
    window.eventUtils=factory();
  }
})(this,function(){
  //...
})

CMD

define(factory)
seajs.use([module],callback)

缺点:依赖SPM打包,模块的加载逻辑偏重

Object和Map的区别

共同点

减值对的动态集合,支持增删改查键值对

var obj={};
obj.a=1;
obj.a=2;
console.log(obj.a)
delete obj.a
const map = new Map();
map.set('a', 1);
map.set('a',2);
map.has('a');
map.delete('a');

不同点

创建方法

Object

var obj={}
var obj1= new Object();
var obj1 = Object.create();

Map

const map = new Map([['a',1],['b',2]]);

键的类型

  • Object的键必须是string或者Symbol
  • Map的键可以任何类型

键的顺序

  • Object
    • key是无序
    • 对于大于等于0的整数,会按照大小进行排序,对于小数和负数会当作字符串处理
    • 对于string类型会按照定义的属性输出
    • 对于Symbol会过滤,得用Object.getOwnPropertySymbols才能得到
  • Map
    • 有序,按照插入的顺序

大小

  • Object通过Object.keys或者for...in遍历
  • Set通过size获取大小

序列号

Object可以序列化和反序列化,Map只能序列化

应用场景

  • Object
    • 数据存储,属性为字符串
    • 需要序列化
    • 当作一个对象的实例,需要保存自己的属性和方法
  • Map
    • 会频繁和更新键值对
    • 存储大量数据,尤其是key未知
    • 需要频繁进行迭代处理

Array和Set的区别

Array

  • 有重复
  • 速度慢 push O(1)unshift O(N) Set
  • 不重复
  • 速度快 O(1)底层是哈希

手写debounce

function debounce(fn, wait, immediate = false){
    let time;
    let isInvoked = false;
    return function(...args){
        clearTimeout(time);
        if(immediate&&!isInvoked){
            fn.apply(this,args);
            isInvoked = true;
        }else{
            time = setTimeout(()=>{
                !immediate&&fn.apply(this,args);
                isInvoked = false;
            },wait)
        }
    }
}

手写throttle

function throttle(fn,wait){
    let time;
    return function(...args){
        if(!time){
            time = setTimeout(()=>{
                fn.apply(this,args);
                time=0;
            },wait)
        }
    }
}

手写eventEmitter

class EventEmitter{
    constructor(){
        this.events = {};
    }
    on(name,fn){
        if(typeof fn !=='function') return;
        this.events[name]?this.events[name].push(fn):this.events[name]=[fn]
    }
    emit(name,...args){
        this.events[name]&&this.events[name].forEach(fn=>fn&&fn(...args))
    }
    once(name,fn){
        const onceFn = (...args) => {
            fn&&fn(...args);
            this.removeListener(name);
        }
        this.on(name, onceFn)
    }
    removeListener(name){
        this.events[name]=[]
    }
}

性能指标有哪些

  • TTFB:Time To First Byte,首字节时间
  • FP:First Paint,首次绘制,绘制 Body
  • FCP:First Contentful Paint,首次有内容的绘制,第一个 dom 元素绘制完成
  • FMP:First Meaningful Paint,首次有意义的绘制
  • TTI:Time To Interactive,可交互时间,整个内容渲染完成
  • LCP:是 Largest Contentful Paint 最大内容绘制
  • TBT 总阻塞时间
  • CLS 累计布局偏移
  • FID 首次输入延迟

FP

白屏时间(First Paint):是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。

中间会经历:DNS 查询、建立 TCP 连接、发送首个 http 请求,返回 html 文档、html 文档 head 解析完毕。

方法一:

通常浏览器认为开始渲染<body>或者解析完<head>的时间是白屏结束的时间点,按照这个思路可以进行代码实现

<head>
  <script>
    window.FPStart = new Date().getTime();
  </script>
  <link ref="" />
  <script src=""></script>
  <script>
    window.FPEnd = new Date().getTime();
    const FP = window.FPEnd - window.FPStart; //白屏时间
  </script>
  <head></head>
</head>

这个方法有个缺点:无法获取解析 HTML 文档之前的时间信息。

方法二

通过 window.performance.timing

domLoading - fetchStart

domLoading:浏览器开始解析网页 DOM 结构的时间。 chStart:浏览器发起 http 请求读取文档的毫秒时间戳。

FCP

首屏时间(First Contentful Paint):是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。

方法一

首屏结束时间应该是页面的第一屏绘制完,但是目前没有一个明确的 API 可以来直接得到这个时间点,所以我们只能智取,比如我们要知道第一屏内容底部在 HTML 文档的什么位置,那么这个第一屏内容底部,也称之为首屏线。不同型号手机屏幕不一样,所以只能估摸大概位置。

FCP 时间在 0-1.8 秒, 表示良好,FCP 评分将在 75~100 分;

FCP 时间在 1.9-3.0 秒, 表示需要改进,FCP 评分将在 50~74 分;

FCP 时间在 3.1 秒以上, 表示较差进,FCP 评分将在 0~49 分。

适用场景:

  • 首屏内不需要拉取数据
  • 不需要考虑图片加载

但是大部分页面都需要用到接口,所以这种方法不常用

方法二

统计首屏最慢图片加载时间 循环首屏每个图片,取图片加载最慢的值。

适用场景:

首屏元素数量固定的页面,比如移动端首屏不论屏幕大小都展示相同数量的内容。

LCP

LCP 时间在 0-2.5 秒, 表示良好;

LCP 时间在 2.6-4.0 秒, 表示需要改进;

LCP 时间在 4.1 秒以上, 表示较差进