Promise

1、介绍下Promise

Promise 是异步编程的一种解决方案,简单说就是一个保存着某个未来才会结束的事件(通常是一个异步操作)结果的容器。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
它有三种状态,pending(进行中)、fulfilled(已成功)、reject(已失败)。注:resolved是指完成状态,结果可能包含fulfilled和rejected

  • 状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态;
  • 一旦状态改变,就不会再变,只有两种可能:从pending变为fulfilled和从pending变为rejected
  • 无法取消,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
2、Promise为了解决什么问题

使用Promise的语法来解决回调地狱的问题,使代码拥有可读性和可维护性。
“回调函数”:把一个函数当作参数传递,传递的是函数的定义并不会立即执行,而是在将来特定的时机再去调用,这个函数就叫做回调函数。
“回调地狱”:把函数作为参数层层嵌套请求,这样层层嵌套,人们称之为回调地狱,代码阅读性非常差。

var sayhello = function (order, callback) {
	setTimeout(function () {
    console.log(order);
    callback();
  }, 1000);
}
sayhello("first", function () {
  sayhello("second", function () {
    sayhello("third", function () {
      console.log("end");
    });
  });
});

使用promise改造

ar sayhello = function (order) {
  return new Promise(function (resolve, reject) {
      setTimeout(function () {
      console.log(order);
      //在异步操作执行完后执行 resolve() 函数
      resolve();  
    }, 1000);
  });
}
sayhello("first").then(function () {
  //仍然返回一个 Promise 对象
  return sayhello("second");  
}).then(function () {
  return sayhello("third");
}).then(function () {
  console.log('end');
}).catch(function (err) {
  console.log(err);
})

从表面上看,Promise只是能够简化层层回调的写法,而实质上Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback 函数要简单、灵活的多。
通过Promise这种方式很好的解决了回调地狱问题,使得异步过程同步化,让代码的整体逻辑与大脑的思维逻辑一致,减少出错率。

3、Promise提供的方法
  • Promise.prototype.then()
    它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。
    then方法返回的是一个新的Promise实例**(注意,不是原来那个Promise实例)**。因此可以采用链式写法,即then方法后面再调用另一个then方法。
  • Promise.prototype.catch(),用于指定发生错误时的回调函数
  • Promise.prototype.finally(),不管 Promise 对象最后状态如何,都会执行的操作,不接受任何参数所以无法知道状态
    finally本质上是then方法的特例。
promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

实现:

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

上面代码中,不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback。

  • Promise.all(),用于将多个 Promise 实例,包装成一个新的 Promise 实例
    如果传入的参数不是Promise,会调用Promise.resolve方法转成Promise实例
    状态变更:
    只有所有promise的状态变更为fulfilled,新的Promise实例才会变成fulfilled
    只要其中一个被rejected,新的Promise实例也会变成reject,此时第一个被reject的实例的返回值,会传递给p的回调函数
  • Promise.race()
    它和Promise.all()的区别是:只要其中有一个实例率先改变状态,新的Promise实例就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给新的Promise实例的回调函数。
  • Promise.allSettled()
    只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。
  • Promise.any()
    只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
  • Promise.resolve()
    将现有对象转为 Promise 对象
  • Promise.reject()
    Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
4、手写Promise

(详见手写promise)[https://developer.aliyun.com/article/613412]
(结合阮一峰的promise和class介绍一起看理解会更深刻一些)[https://es6.ruanyifeng.com/#docs/promise]

class Promise {
  // constructor为构造方法,通过new命令生成对象实例时,自动调用该方法
  // this是实例对象
  constructor(executor) {
    // 初始状态
    this.state = 'pending'
    // 成功的返回值
    this.value = undefined
    // 失败原因
    this.reason = undefined
    // 成功存放的数组
    this.onResolvedCallbacks = []
    // 失败存放的数组
    this.onRejectCallbacks = []

    const resolve = value => {
      // 调用resolved后,状态需要变更
      if (this.state === 'pending') {
        this.state = 'fulfilled'
        this.value = value
        // 一旦执行resolve,调用成功数组的函数
        this.onResolvedCallbacks.forEach(fn => fn())
      }
    }
    const reject = reason => {
      // 调用resolved后,状态变更为失败
      if (this.state === 'pending') {
        this.state = 'rejected'
        this.reason = reason
        this.onRejectCallbacks.forEach(fn => fn())
      }
    }
    try {
      executor(resolve, reject)
    } catch (err) {
      // 如果执行错误,直接把错误返回
      reject(err)
    }
  }
  // then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。
  then (onFulfilled, onRejected) {
    // 如果onFulfilled不是函数,直接返回value
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}
    // 如果状态为成功,执行onFulfilled,传入成功值
    let promise2 = new Promise((resolve, reject)=> {
      // onFulfilled和onReject只能异步调用
      if (this.state === 'fulfilled') {
        setTimeout(()=> {
          try{
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch(err) {
            reject(err)
          }
        },0)
      } else if (this.state === 'rejected') {
        setTimeout(()=> {
          try{
            let x = onFulfilled(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch(err) {
            reject(err)
          }
        },0)
      } else if (this.state === 'pending') {
        this.onResolvedCallbacks.push(()=> {
          setTimeout(()=> {
            try{
              let x = onFulfilled(this.value)
              resolvePromise(promise2, x, resolve, reject)
            } catch(err) {
              reject(err)
            }
          },0)
        })
        this.onRejectCallbacks.push(()=> {
          setTimeout(()=> {
            try{
              let x = onFulfilled(this.reason)
              resolvePromise(promise2, x, resolve, reject)
            } catch(err) {
              reject(err)
            }
          },0)
        })
      }
    })
    return promise2
  }
  // Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
  catch() {

  }
}

function resolvePromise(promise2, x, resolve, reject) {
  // x不能等于promise2,会循环饮用报错
  if (x === promise2) {
    return reject(new TypeError('Chaining cycle detected for promise'))
  }
  // 防止重复调用
  let called
  // 如果为普通类型直接resolve
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then
      // then是函数,默认为promise
      if (typeof then === 'function') {
        then.call(x,y => {
          if (called) return 
          called = true
          // resolve的结果依旧是promise 那就继续解析
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if (called) return 
          called = true
          reject(err)
        })
      } else {
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(x)
  }
}

// resolve、catch、reject、race、all方法不在promise/A+规范中,均为ES6实现的方法

Promise.resolve = function (val) {
  return new Promise((resolve, reject)=> {
    resolve(val)
  })
}

Promise.reject = function (val) {
  return new Promise((resolve, reject)=>{
    reject(val)
  })
}

// 第一个完成的promise函数状态传递给Promise.race的结果
Promise.race = function (promises = []) {
  return new Promise((resolve, reject) => {
    promises.forEach(fn => {
      // 如果传入的参数为promise
      if (fn && typeof fn === 'function') {
      // 谁先执行完到then,使用第一个执行完成的结果
      fn.then(resolve, reject) // 这里不理解可以看看前面then的实现
      } else {
        Promise.resolve(fn).then(resolve, reject)
      }
    })
  })
}

Promise.all = function (promises = []) {
  const promiseRes = []
  // 主要需要实现两个功能,1、所有promise的状态都变成fulfilled才会变成fulfilled,其中要一个被rejected,状态就会变成reject;
  // 2、返回的参数是按照传入的顺序而不是完成的时间先后
  return new Promise((resolve, reject) => {
    promises.forEach((fn, index) => {
      // 可以加入传入参数不为promise的判断处理,见race方法的实现
      fn.then(res => {
        promiseRes.splice(index, 0, res)
        if (promiseRes.length = promises.length) {
          resolve(promiseRes)
        }
      }, err => {
        reject(err)
      })
    })
  })
}
5、promise怎么实现的异步队列,怎么实现的链式调用

理解了上面的手写promise,就可以轻易回答这个问题
异步队列:
promise 的本质是回调函数,then 方法的本质是依赖收集,它把 fulfilled 状态要执行的回调函数放在一个队列, rejected 状态要执行的回调函数放在另一个队列。待 promise 从 pending 变为 fulfilled/rejected 状态后,把相应队列的所有函数,执行一遍。
链式调用:
then方法返回的是一个新的Promise实例。所以可以采用链式写法,将返回结果作为参数,传入第二个回调函数

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});
6、await和promise的关系,分别的应用场景有哪些,有什么区别

async 函数是 Generator 函数的语法糖,是一种异步编程解决方案。
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
异步函数的语法结构更像是标准的同步函数,发明了async和await的初衷就是让异步代码的语法结构跟同步代码类似。相对promise,async的实现最简洁,最符合语义
应用场景:await能解决的问题,promise其实也可以,但是在一些简单的异步场景,await会更加简洁,更具语义化;另外promise提供了很多方法,比如all,race这些能满足更多场景的使用

区别:
1、await代码阅读性比较强,像同步代码,promise书写方式是链式的,容易造成代码多层堆叠难以维护。
2、await使用try、catch可以处理同步和异步的错误;promise使用then().catch()去处理数据和捕获异常

7、promise 有几种状态,Promise 有什么优缺点

有三种状态,pengding、fulfilled、rejected。
优点:
(1)解决回调地狱问题 (2)更好地进行错误捕获 (3)提高代码可读性
缺点:
(1)无法取消Promise,一旦新建它就会立即执行,无法中途取消。
(2)如果不设置回调函数,promise内部抛出的错误,不会反应到外部。
(3)当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

8、如何实现 Promise.finally

不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};
9、async/await了解么,generator用过么

generator:
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。ES6 诞生以前,异步编程的方法大概有四种,回调函数、事件监听、发布/订阅、Promise 对象。Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。
语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态
遍历器对象的next方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
async/await:
async 其实就是 Generator 函数的语法糖。将 Generator 函数的星号(*)替换成async,将yield替换成await
async函数对 Generator 函数的改进,体现在以下四点。
1)内置执行器。Generator 函数的执行必须靠执行器,await会自动执行
2) 更好的语义。比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
3)更广的适用性。await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
4)返回值是 Promise。async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了

10、如何限制 Promise 请求并发数

限制 Promise 请求的并发数可以通过控制同时进行的异步操作数量来实现。常见的实现方法是使用队列和计数器,下面提供了一种简单的实现方式。

方法
创建一个队列:将所有的异步任务放入一个队列中。
设置并发限制:定义一个变量来控制当前正在执行的任务数量。
处理队列中的任务:每当有任务完成时,从队列中取出下一个任务并开始执行,直到达到并发限制。
示例代码
以下是一个示例实现,限制同时进行的请求数量:

function limitConcurrentPromises(promises, limit) {
    let index = 0; // 当前正在处理的任务索引
    let activeCount = 0; // 当前正在进行的任务数量
    const results = []; // 存储结果

    return new Promise((resolve, reject) => {
        const next = () => {
            // 如果所有任务都已处理,且没有正在进行的任务,解决 Promise
            if (index >= promises.length && activeCount === 0) {
                return resolve(results);
            }

            // 如果当前正在进行的任务数量小于限制,继续处理下一个任务
            while (activeCount < limit && index < promises.length) {
                const currentIndex = index++;
                activeCount++;

                // 执行当前任务
                promises[currentIndex]().then(result => {
                    results[currentIndex] = result; // 保存结果
                    activeCount--; // 减少正在进行的任务计数
                    next(); // 处理下一个任务
                }).catch(error => {
                    reject(error); // 处理错误
                });
            }
        };

        // 开始处理任务
        next();
    });
}

// 示例使用
const createPromise = (value, time) => {
    return () => new Promise((resolve) => {
        setTimeout(() => {
            console.log(`Resolved: ${value}`);
            resolve(value);
        }, time);
    });
};

const promises = [
    createPromise('Task 1', 1000),
    createPromise('Task 2', 500),
    createPromise('Task 3', 300),
    createPromise('Task 4', 700),
    createPromise('Task 5', 600)
];

limitConcurrentPromises(promises, 2)
    .then(results => {
        console.log('All tasks completed:', results);
    })
    .catch(error => {
        console.error('Error:', error);
    });

事件循环机制

详见 事件循环分为浏览器事件循环和node.js事件循环
浏览器的事件循环分为同步任务和异步任务;所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈),而异步则先放到任务队列(task queue)里,任务队列又分为宏任务(macro-task)与微任务(micro-task)。下面的整个执行过程就是事件循环

宏任务大概包括::script(整块代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(node环境)

微任务大概包括::new promise().then(回调)、MutationObserver(html5新特新)、Object.observe(已废弃)、process.nextTick(node环境)

若同时存在promise和nextTick,则先执行nextTick

执行过程

JS 引擎去执行 JS 代码的时候会从上至下按顺序执行,先把同步任务放入执行栈中立即执行,微任务放入微任务队列,宏任务放在宏任务队列。当执行栈被清空,然后去执行所有的微任务,当所有微任务执行完毕之后。再次从宏任务开始循环执行,直到执行完毕,然后再执行所有的微任务,就这样一直循环下去。如果在执行微队列任务的过程中,又产生了微任务,那么会加入整个队列的队尾,也会在当前的周期中执行。其实浏览器执行Js代码的完整顺序应该是:同步任务 ——> 异步微任务 ——> DOM渲染页面 ——>异步宏任务

es超内存_前端

1、nextTick,setTimeout和setImmediate的区别

process.nextTick(),效率最高,消费资源小,但会阻塞CPU的后续调用; process.nextTick()方法可以在当前"执行栈"的尾部–>下一次Event Loop(主线程读取"任务队列")之前–>触发process指定的回调函数。也就是说,它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。(nextTick虽然也会异步执行,但是不会给其他io事件执行的任何机会)
setTimeout(),精确度不高,可能有延迟执行的情况发生,且因为动用了红黑树,所以消耗资源大; setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
setImmediate(),消耗的资源小,也不会造成阻塞,但效率也是最低的。setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行setImmediate指定的回调函数,和setTimeout(fn,0)的效果差不多,但是当他们同时在同一个事件循环中时,执行顺序是不定的。
(参考)[]
(部分题目)[]

2、执行顺序题目

1、

console.log('1')
setTimeout(() => {
    console.log('4')
    setTimeout(() => {
        console.log('7')
    }, 0)
   Promise.resolve()
     .then(() => {
         console.log('6')
     })
    console.log('5')
}, 0)
Promise.resolve()
    .then(() => {
        console.log('3')
    })
console.log('2')

输出 1,2,3,4,5,6,7
2、

console.log(1);
setTimeout(()=>console.log(2));
new Promise((resolve, reject)=>{
  Promise.resolve(3).then((result)=>{
    console.log(result);
  });
  resolve();
  console.log(4);
}).then((result)=>{
  console.log(result);
}, (error)=>{
  console.log(error);
});
console.log(5);

输出:1 4 5 3 undefined 2
3、

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})
const promise2 = promise1.then(() => {
  throw new Error('error!!!')
})
 
console.log('promise1', promise1)
console.log('promise2', promise2)
 
setTimeout(() => {
  console.log('promise1', promise1)
  console.log('promise2', promise2)
}, 2000)

输出:

promise1 Promise { <pending> }
promise2 Promise { <pending> }
(node:50928) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: error!!!
(node:50928) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
promise1 Promise { 'success' }
promise2 Promise {
  <rejected> Error: error!!!
    at promise.then (...)
    at <anonymous> }

4、

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('once')
    resolve('success')
  }, 1000)
})
 
const start = Date.now()
promise.then((res) => {
  console.log(res, Date.now() - start)
})
promise.then((res) => {
  console.log(res, Date.now() - start)
})

输出: (答案不唯一,promise forEach消耗的时间会有差异)
once
success 1002
success 1002
5、

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

输出:1(.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。)
6、

const promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    console.log("timerStart");
    resolve("success");
    console.log("timerEnd");
  }, 0);
  console.log(2);
});
promise.then((res) => {
  console.log(res);
});
console.log(4);

输出:1、2、4、timerStart、timerEnd、success

7、浏览器事件循环的异步任务为什么要分成微任务和宏任务?
为了可以插队。
在事件循环机制中,异步任务被分为宏任务和微任务两类,分别被放置到宏任务队列和微任务队列中,然后被依次执行。
宏任务通常包括以下几类:

  • setTimeout 和 setInterval 定时器回调函数
  • DOM 事件回调函数
  • Ajax 请求的回调函数
  • 手动调用的通过 setTimeout、setInterval、setImmediate 等方法创建的回调函数

而微任务则通常包括以下几类:

  • Promise.then() 和 catch() 方法的回调函数
  • async/await 函数生成器的 yield 命令后的代码
  • MutationObserver 监听器的回调函数

宏任务和微任务被放置在不同的队列中,宏任务的执行时机在当前事件循环的末尾,微任务的执行时机在当前宏任务执行完毕后、下一个宏任务开始执行前。
在实际使用中,由于微任务在宏任务执行完毕后立即执行,所以它们可以在 DOM 更新之前执行。因此,使用微任务可以使得 DOM 更新更加及时,提高用户体验。而宏任务的执行则相对较慢,适合执行一些相对耗时的任务,比如网络请求和计时器任务等。

8、为什么浏览器需要循环事件
1)因为js是单线程,同时只能执行一个任务,event loop可以确保JavaScript代码的执行顺序,使得代码能够按照预期的顺序进行执行,同时不会被阻塞。如果没有事件循环机制,那么当JavaScript执行时间较长时,会导致浏览器的UI线程被阻塞,用户无法操作页面,甚至会出现页面卡顿、崩溃等情况。

2)在浏览器中,渲染和事件处理都是由主线程来执行的,主线程负责执行JavaScript代码,计算布局、样式和渲染页面,并处理事件等操作。由于JavaScript是单线程的,当JavaScript执行较长时间时,页面的渲染和事件处理会被阻塞,用户体验会变得非常差。
为了解决这个问题,浏览器采用了异步的机制。事件循环就是其中一种机制。它可以使得JavaScript执行一段代码后,立即返回到主线程执行其他操作,等到异步任务执行完毕之后,再回到事件循环中执行回调函数。这样就能避免JavaScript长时间阻塞主线程,从而提高页面渲染和事件处理的性能。

ES6

1、ES6 语法用过哪些,都有哪些常用的特性

  • let、const;1、不存在变量提升;2、封闭作用域,所在的代码块内有效;3、暂时性死区”,在代码块内,使用let命令声明变量之前,该变量都是不可用的。4、不允许重复声明;
    const实际上并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动,即不能改动简单类型的数据(数值、字符串、布尔值),可以改动复合类型的数据(主要是对象和数组)。多使用const,有利于提高程序的运行效率
  • 变量解构;指按照一定模式,从 数组和对象中提取值,对变量进行赋值。
  • 模板字符串;用反引号(`)标识 ,将变量名写在${}之中
  • 箭头函数;
  • 链判断运算符?.;Null 判断运算符; 0 || 1,算符左侧的值为null或undefined时,才会返回右侧的值。
  • symbol;通过Symbol()函数生成一个独一无二的值,可以保证对象属性名不会产生冲突。只能用Object.getOwnPropertySymbols()方法遍历出symbol值
  • Set、Map;Set类似于数组,但是成员的值都是唯一的。Map类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键
  • Proxy;可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写;一共支持 13 种拦截操作,常用的是get,set;proxy相较ES5Object.defineProperty的不同,1、直接监听对象⽽⾮属性 2、直接监听数组的变化 3、拦截⽅式较多(有 13 种⽅式)4、Proxy 返回⼀个新对象,可以只操作新对象⽬的,⽽ Object.defineProperty 只能遍历对象属性
  • Promise;异步方程的解决方案
  • async; 是generator函数的语法糖,使异步操作变得更方便
  • class,ES6的类,完全可以看作构造函数的另一种写法。
  • module;1、可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。2、模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。3、import命令具有提升效果,会提升到整个模块的头部,首先执行4、ES6 module是编译时加载 5、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。6、AMD是为了解决CommonJS只能同步加载而诞生的标准。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。也采用require()语句加载模块,但是多两个参数,第一个是加载的模块,第二个是加载成功后回调的函数require([module], callback)。
    2、ES6和ES5的继承
    ES5和ES6继承JS原型链与继承别再被问倒了 一般有构造函数继承、原型链继承、组合式继承、寄生式继承、组合继承式继承
    组合式继承:组合继承是 JavaScript 最常用的继承模式; 不过, 它也有自己的不足. 组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部. 寄生组合式继承就是为了降低调用父类构造函数的开销而出现的 。
function Parent(name) {
    this.name = name || 'parent';
}
function Child(name, age) {
    Parent.call(this, name); //继承实例属性,第一次调用Parent()
    this.age = age;
}
Child.prototype = new Parent(); //继承原型,第二次调用Parent()
Child.prototype.constructor = Child;//修正构造函数为自己本身

寄生组合式继承:所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function extend(subClass, superClass) {
    var prototype = Object(superClass.prototype); // 创建对象,创建父类原型的一个副本
    prototype.constructor = subClass; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
    subClass.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
function Parent(name) {
    this.name = 'allen';
}
function Child(name) {
    Parent.call(this, name); //继承第一步,继承实例属性,调用Parent()
}
extend(Child, Parent); //继承第二步,不会调用Parent()

3、箭头函数与普通函数的区别

  • 箭头函数没有自己的this对象(详见下文 )。
  • 不可以当作构造函数,也就是说,不可 以对箭头函数使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

JS基础

1、js的数据类型都有哪些 ,有什么区别,数据类型常用的判断方式都有哪些,为什么基本数据类型存到栈但是引用数据类型存到堆
基础数据类型:Number、String、Boolean、Null、Undefined、Symbol
引用数据类型:Object、Array、Function
常用判断方式:

  • typeof,由于历史原因typeof null和[]返回的都是是object,另外判断值是否声明用typeof 变量 === “undefined”
  • object.property.toString.call方法 ,返回"[object, 类型],适用于所有数据类型判断
  • instanceof,只能用于判断复杂数据类型
    基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问。
    引用类型是存放在堆内存中的对象,变量其实是保存的在栈内存中的一个指针(保存的是堆内存中的引用地址),这个指针指向堆内存。引用数据类型数据创建的时候大小不确定。
    堆比栈大,栈比堆的运算速度快,对象是一个复杂的结构,并且可以自由扩展,将他们放在堆中是为了不影响栈的效率。简单数据类型就比较稳定,并且它只占据很小的内存,放在栈中能提高效率

2、闭包
闭包就是能够读取其他函数内部变量的函数。可以理解成“定义在一个函数内部的函数“。当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
特点:

  • 闭包会使函数中的变量都保存在内存中,不会被垃圾回收机制回收,占用内存导致内存泄露;解决方法是,在退出函数之前,将不使用的局部变量全部删除。
    学习Javascript闭包

3、原型链讲一下
原型:原型就是一个对象,它存在于 Person.prototype 中,用来存放共享的方法和属性,供所有由 Person 构造函数创建的对象使用。

function Person(name) {
  this.name = name; // 实例自己的属性
}

Person.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.name); // 原型上的方法
}

const person1 = new Person("Alice");

原型链:当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__(隐式原型)属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象(可以理解为爷爷对象)里找,这种通过__proto__属性来连接对象直到null的一条链即为我们所谓的原型链。

原型链详解

es超内存_es超内存_02


4、esmodule和commonjs区别是什么,还接触过其他的模块化方案么

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段

6、设计模式
前端比较常见的是单例模式、观察者模式、代理模式。
单例模式:指保证一个类仅有一个实例,并提供一个访问它的全局访问点。常见是用于命名空间;Vuex、Redux也采纳了单例模式,两者都用一个全局的惟一Store来存储所有状态。

  • 单例模式能保证全局的唯一性,可以减少命名变量
  • 单例模式在一定情况下可以节约内存,减少过多的类生成需要的内存和运行时间
  • 把代码都放在一个类里面维护,实现了高内聚
    观察者模式:定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。比如当数据有更新,changed 方法会被调用
  • 观察者和被观察者是 抽象耦合 的
  • 建立一套触发机制
    代理模式:指为一个原对象找一个代理对象,以便对原对象进行访问。即在访问者与目标对象之间加一层代理,通过代理做授权和控制。事件委托/代理,ES6 的 proxy 都是这一模式的实现。
  • 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  • 远程代理使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
  • 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
  • 保护代理可以控制对真实对象的使用权限。

7、process.env.NODE_ENV是什么?说一下 Process ,以及 Require 原理?
在node中,有全局变量process表示的是当前的node进程。
process.env包含着关于系统环境的信息,但是process.env中并不存在NODE_ENV。
NODE_ENV是一个用户自定义的变量,在webpack中它的用途是判断生产环境或开发环境。

8、Object.create(null)和直接创建一个{}有什么区别
Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型。
Object.create(null) 创建一个空对象,此对象无原型方法。
{} 其实是new Object(),具有原型方法。

10、异步加载js的方式都有哪些
defer,始终在页面渲染后(dom树生成后)再执行js
async,js加载完后立即执行,可能会阻塞渲染
按需加载

<script>
		// url是按需加载的js文件,callback是按需加载的js文件中某个函数
        function loadScript(url,callback){
            var script = document.createElement('script');    
            // 处理ie的兼容
            if(script.readyState){
                script.onreadystatechange = function(){
                    if(script.readyState == 'complete' || script.readyState == 'loaded'){
                        callback();
                    }
                }
            }else{
                script.onload = function(){
                    callback();
                }
            }
            script.src = url; // 给script标签添加src 引入一个js文件
            document.body.appendChild(script); // 追加到body
        }
    </script>

11、判断一个对象是否是循环引用对象
循环引用是指对象的地址和源的地址相同,它只会发生在Object等引用类型的数据中

// 循环引用
const a = {};
a.b = a

实现一个方法判断是否是循环引用对象,具体思路是遍历对象的值是否存在与源的地址相同的情况

function cycle(obj, parent) {
    //表示调用的父级数组
    var parentArr = parent || [obj];
    for (var i in obj) {
        if (typeof obj[i] === "object") {
            //判断是否有循环引用
            parentArr.forEach((pObj) => {
                if (pObj === obj[i]) {
                    obj[i] = "[cycle]"
                }
            });
            cycle(obj[i], [...parentArr, obj[i]])
        }
    }
    return obj;
}

12、跨域,img标签为什么没有跨域问题
浏览器要求,在解析Ajax请求时,要求浏览器的路径与Ajax的请求的路径必须满足三个要求,则满足同源策略,可以访问服务器
协议、域名、端口号都相同才为同源,否则会跨域
如果服务器没有配置CORS,则简单跨域请求可以成功执行,会返回200状态码,但返回的内容会被浏览器拦截!
解决跨域的方法:

  • CORS;服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源。
  • JSONP; script、img、link、iframe … 这些标签不存在跨域请求的限制,就是利用这个特点解决跨域问题。核心思想:网页通过添加一个
this.x = 9;    // 在浏览器中,this 指向全局的 "window" 对象
var module = {
  x: 81,
  getX: function() { return this.x; }
};
module.getX(); // 81
var retrieveX = module.getX;
retrieveX();
// 返回 9 - 因为函数是在全局作用域中调用的
// 创建一个新函数,把 'this' 绑定到 module 对象
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81

1、call、bind、apply使用目的上都是一样的,都是将一个构造方法当作一个普通方法调用;均传递是一个this域对象;均可传递参数。
2、不同点: call、apply直接调用,bind返回一个新函数需手动调用;apply的参数为数组形式,call、bind为单个参数
手写call:(与apply的不同仅为处理参数时, let args = arguments[1] )

Function.prototype.myCall = function(context) {
  // 判断是否是undefined和null
  if (typeof context === 'undefined' || context === null) {
    context = window
  }
  // call一个参数是其改变指向的对象,后续参数作为实参传递给调用者。所以这里用[...arguments].slice(1)获取到了传递给调用者的函数参数。
  let args = [...arguments].slice(1)  
  context.fn = this  // 将调用的函数设置为参数context的方法
  let result = context.fn(...args)  // 调用函数
  delete context.fn // 移除属性
  return result // 返回结果
}

手写bind:

Function.prototype.myBind = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  let _this = this
  let args = [...arguments].slice(1)
  return function F() {
    // 判断是否被当做构造函数使用
    if (this instanceof F) {
      return _this.apply(this, args.concat([...arguments]))
    }
    return _this.apply(context, args.concat([...arguments]))
  }
}

15、Map
Map 数据结构类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
包含以下属性和操作方法:

  • size 属性;返回 Map 结构的成员总数。
  • Map.prototype.set(key, value)
  • Map.prototype.get(key)
  • Map.prototype.get(key);has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
  • Map.prototype.delete(key);delete方法删除某个键,返回true。如果删除失败,返回false。
  • Map.prototype.clear();clear方法清除所有成员,没有返回值。
    遍历方法:
  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

17、手写instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上object instanceof constructor;object为 某个实例对象、constructor 为某个构造函数
instanceof可以用于判断复杂数据类型

function myInstanceOf(object, constructor) {
    if (obj === null || type obj !== 'object' || typeof constructor !== 'function') return false
    let pointer = object._proto_
    while (pointer !== null){
        if (pointer === constructor.prototype) return true
        else pointer = pointer._proto_
    }
}

18、0.1 + 0.2 为什么不等于 0.3
因为在 0.1+0.2 的计算过程中发生了两次精度丢失。第一次是在 0.1 和 0.2 转成双精度二进制浮点数时,由于二进制浮点数的小数位只能存储52位,导致小数点后第53位的数要进行为1则进1为0则舍去的操作,从而造成一次精度丢失。第二次在 0.1 和 0.2 转成二进制浮点数后,二进制浮点数相加的过程中,小数位相加导致小数位多出了一位,又要让第53位的数进行为1则进1为0则舍去的操作,又造成一次精度丢失。最终导致 0.1+0.2 不等于0.3 。

19、js 垃圾回收机制(GC)
采用的两种方式:
1)、标记清除
当变量进入执行环境时,就将这个变量标记为“进入环境”,当变量离开环境时会被标记“离开环境”,离开环境的变量内存被释放

function f1(){
       //被标记已进入执行环境
       var a=1
       var b=2
   }
   f1()  //执行完毕,a,b被标记离开执行环境,内存释放

2)、引用计数
跟踪记录每个值被引用的次数,当某个值的引用次数变为0时,说明没有方法在访问该值了,则可将其占用的内存收回

function f1(){
     //跟踪a的引用计数
     var a={}   //a的引用次数 0
     var b=a    //a的引用次数 1
     var c=a     //a的引用次数 2
     var b={}    //a的引用次数 1
     var c=[]    //a的引用次数 0
 }

3)、手工 --直接置空,GC下次再运行时会删除这些值

a=null

20、=的区别
1、对于 string、number 等基础类型,== 和 === 是有区别的
a)不同类型间比较,== 之比较 “转化成同一类型后的值” 看 “值” 是否相等,=== 如果类型不同,其结果就是不等。
b)同类型比较,直接进行 “值” 比较,两者结果一样。
2、对于 Array,Object 等高级类型,== 和 === 是没有区别的
进行 “指针地址” 比较
3、基础类型与高级类型,== 和 === 是有区别的
a)对于 ,将高级转化为基础类型,进行 “值” 比较
b)因为类型不同,
= 结果为 false

21、XHR 和 Fetch的区别

  • Fetch会发送两次请求,使用fetch发送POST请求时,会先发送一个OPTION请求进行预检查,用来获知是否支持请求头等操作,服务器确认允许之后会返回204状态码,表示允许该跨域请求,这时才发起实际的 HTTP 请求。
  • fetch是原生js方法,调用比较简单,会返回一个promise,xhr需要新增一个XHMHttpRequest的实例
fetch(url).then(function(response){
    return response.json();
}).then(function(jsonData){
    console.log(jsonData);
}).catch(function(){
    console.log('something wrong~ ╮( ̄▽ ̄)╭');
var xhr = new XHMHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function(){
    console.log(xhr.response);
};
xhr.onerror = function(){
    console.log('something wrong~ ╮( ̄▽ ̄)╭');
};
xhr.send();

22、samesite
Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击 和用户追踪(第三方恶意获取cookie),限制第三方 Cookie,从而减少安全风险。
SameSite属性可以设置三个值:Strict、Lax、None。

  • Strict:严格,完全禁止第三方获取cookie,跨站点时,任何情况下都不会发送cookie;只有当前网页的 URL 与请求目标一致,才会带上 Cookie。这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
  • Lax:防范跨站,大多数情况下禁止获取cookie,除非导航到目标网址的GET请求(链接、预加载、GET表单);设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。

SameSite属性的默认SameSite=Lax 【该操作适用于2019年2月4号谷歌发布Chrome 80稳定版之后的版本】

Set-Cookie: CookieName=CookieValue; SameSite=Lax;
  • None:没有限制。

23、判断类型

  • typeof
    1)返回结果只有以下几种:number,string,boolean,object,undfined,function
    2)无法判断对象和数组,还有null,因为都返回的是object
  • instanceof
    instanceof 是用来 判断数据是否是某个对象的实例,返回一个布尔值。
    对于基本类型的数据,instanceof是不能直接判断它的类型的,因为实例是一个对象或函数创建的,是引用类型,所以需要通过基本类型对应的 包装对象 来判断。所以无法判断 null 和 undefined。
    因为原型链继承的关系,instanceof 会把数组都识别为 Object 对象,所有引用类型的祖先都是 Object 对象
5 instanceof Number // false
new Number(5) instanceof Number  // true
  • Object.prototype.toString.call()
    在判断数据类型时,我们称 Object.prototype.toString 为 “万能方法” “终极方法”,工作中也是比较常用而且准确。
    对于Object.prototype.toString() 方法,会返回一个形如 “[object XXX]” 的字符串

24、如何判断属性是对象实例中的属性还是原型中的属性
1)hasOwnProperty()函数用于判断只在属性存在与对象实例中的时候,返回true。
2)(属性名称 in 对象) 不管属性是原型的还是实例的,只要存在就返回ture否则返回false

!obj.hasOwnProperty(name) && (name in obj)

代码的意思是不在name不在obj的实例中存在并且那么在原型/实例中,所以当属性存在原型上的时候,函数返回true

25、手写一个单例模式
单例模式是一种常见的设计模式,它确保一个类只有一个实例,并提供一个全局访问点。

实现单例模式的关键是使用一个变量来保存实例,并通过一个公共方法来获取这个实例。在获取实例时,首先判断变量是否已经保存了实例,如果已经保存则直接返回该实例,否则创建一个新的实例,并保存到变量中。

以下是一个手写的单例模式的例子:

class Singleton {
  constructor() {
    // 如果已经有实例了,则返回该实例
    if (Singleton.instance) {
      return Singleton.instance;
    }
    // 如果没有实例,则创建一个实例,并保存到 Singleton.instance 变量中
    Singleton.instance = this;
  }
}

const s1 = new Singleton();
const s2 = new Singleton();
console.log(s1 === s2); // true

应用:

  • Vuex 状态管理:Vuex 的 store 对象就是一个单例,它用于存储全局共享的状态,保证多个组件共享同一个状态对象。
  • 弹窗组件:弹窗组件通常只会有一个实例,可以使用单例模式来确保只有一个实例存在,避免多个弹窗重复弹出的问题。

26、手写防抖、节流

// 防抖
function debounce(fn, delay) {
  let timer = null;
  return function() {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
    fn.call(this, arguments)
    }, delay)
  }
}
// 节流
function throttle(func, delay) {
  let timer = null;
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, arguments);
        timer = null;
      }, delay);
    }
  };
}

防抖和节流区别:

防抖是触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。适用于可以多次触发但触发只生效最后一次的场景。

节流是高频事件触发,但在n秒内只会执行一次,如果n秒内触发多次函数,只有一次生效,节流会稀释函数的执行频率。

27、继承的几种方式
继承的六种方式 1)、ES6

class Animals {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  getAge() {
    console.log(this.age)
  }
}

class dog extends Animals {
  constructor(name, age, color) {
    super(name, age)
    this.color = color
  }
  getName() {
    console.log('小' + super.name)
  }
}

2)、原型链继承
缺点:只能继承父类原型上的方法和属性,不能继承父类的实例属性和方法,多个实例对引用类型的操作会被篡改。

function Animals(name) {
	this.name = name 
}
Animals.getAge = () => {
	console.log(this.age)
}

function Dog(color) {
	this.color = color
}

Dog.prototype = new Animals()
Dog.prototype.constructor = Dog;

3)、

28、实现call、apply、bind

let target = {
    name: '小周',
    age: 19,
    say: function(school, grade) {
        console.log(`${this.name}今年${this.age},在${school}读${grade}`)
    }
}

let myTarget = {
    name: '小刘',
    age: 17
}
1、call模拟代码;

Function.prototype.myCall = function(target) {
 // 如果不穿值,则默认为this指向window
 let obj = target || window;
 obj.fn = this;
 const objArguments = [...arguments].splice(1);
 const result = obj.fn(...objArguments);
 delete obj.fn;
 return result;
}
2、apply模拟代码

Function.prototype.myBind = function(target) {
    const _this = this;
    let objArguments = [];
    objArguments = [...arguments].splice(1);
    return function(){
        _this.apply(target, objArguments)
    }
}
3、bind模拟代码

Function.prototype.myBind = function(target) {
    const _this = this;
    let objArguments = [];
    objArguments = [...arguments].splice(1);
    return function(){
        _this.apply(target, objArguments)
    }
}


target.say.myCall(myTarget, '一中', '高二');  // 小刘今年17,在一中读高二
target.say.myApply(myTarget, ['一中', '高二']); // 小刘今年17,在一中读高二
target.say.myBind(myTarget, '一中', '高二')(); // 小刘今年17,在一中读高二