一、 几个典型问题及答案  

 1、如果我们在浏览器控制台中运行'foo'函数,是否会导致堆栈溢出错误?

 

    function foo(){

    setTimeout(foo,0) //是否存在堆栈溢出错误?

   }

   答案解析:不会溢出

  javascript 并发模型基于"事件循环"。我们执行js是靠浏览器提供运行环境

 浏览器的主要组件包括调用堆栈,事件循环**,任务队列和 Web API **。像setTimeout,setInterval 和 Promise 这样的全局函数不是javascript 的一部分,而是 web API

 的一部分。

 引擎每次从堆栈中取出一个函数,然后从上到下依次运行代码。每当遇到一些异步代码,如setTimeout ,就把它交给web API 。因此,每当事件被触发时,callback都会

 被发送到任务队列。

 事件循环不断监听任务队列,并按它们排队的顺序一次处理一个回调。每当调用堆栈为空时,Event loop 获取回调并将其放入堆栈。

   下面来回答问题:

   1)调用foo() 会将函数放入调用堆栈

   2)在处理内部代码时,JS引擎遇到setTimeout

   3) 然后将 foo 回调函数传递给 Web API 并从函数返回,调用堆栈再次为空。

   4)计时器被设置为0,因此foo 将被发送到任务队列。

   5) 由于调用堆栈是空的,事件循环将选择foo 回调并将其调用堆栈进行处理

   6)进程再次重复,堆栈不会溢出

 

2、如果在控制台中运行以下函数,页面(选项卡)的UI 是否仍然响应

    function foo(){

      return Promise.resolve().then(foo);

    }

  答案: 不会响应

 解析: JS是一种单线程的语言,它的事件执行顺序是由event loop 来进行调度的

    

  如图所示:

   1)同步和异步分别进入不同的执行场所,同步的进入主线程,异步的进入Event Table 并注册函数

   2)  当指定的事情完成时,Event Table 会将这个函数移入 Event Queue。

   3)主线程的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行

   4) 上述过程不断重复,也就是常说的 Event Loop (事件循环)

 

  现在回到我们的问题上,在底层,javascript 中有宏任务和微任务。setTimeout 回调是宏任务,而Promise 回调是微任务。

  主要区别在于他们的执行方式。宏任务在单个循环周期中一次一个地推入堆栈,但是微任务队列总是在执行后返回到事件循环

  之前清空。因此,如果你以处理条目的速度向这个队列添加条目,那么你就永远在处理微任务。只有当微任务为空时,事件循环才会重新渲染页面

  当你在控制台中运行上述代码段,每次调用'foo'都会继续在微任务队列上添加另一个'foo'回调,因此事件循环无法继续处理其他事件。直到该队列

  完全清空为止。因此,它会阻止渲染。

 

 3、 我们能否以某种方式为下面语句使用展开运算而不导致类型错误   

    var obj = { x:1,y:2,z:3};

    [...obj]; // TypeError

    展开语法 和 for-of 语句遍历iterable 对象定义要遍历的数据。Array 或 Map 是具有默认迭代行为的内置迭代器。对象不是可迭代的,但可以通过

    使用 iterable 和  iterator 协议使它们可迭代。

    如果一个对象实现了@@iterator方法,那么它就是可迭代的,这意味着这个对象(或者它原型链上的一个对象)必须有一个带有@@iterator键的属性,

   这个键可以通过常量symbol.iterator 获得,

   

  var obj = { x: 1, y: 2, z: 3 }; obj[Symbol.iterator] = function() { // iterator 是一个具有 next 方法的对象, // 它的返回至少有一个对象 // 两个属性:value&done。 // 返回一个 iterator 对象 return { next: function() { if (this._countDown === 3) { const lastValue =    this._countDown; return { value: this._countDown, done: true }; } this._countDown = this._countDown + 1; return { value: this._countDown, done: false }; }, _countDown: 0 }; }; [...obj]; // 打印 [1, 2, 3]
 
  也可以通过 generator 函数来定制对象的迭代行为
 
  var obj = {x:1, y:2, z: 3}  obj[Symbol.iterator] = function*() { yield 1; yield 2; yield 3; } [...obj]; // 打印 [1, 2, 3]

 

    对generate 函数的补充
       function* helloGenerator() {
          yield "hello";
          yield "generator";
          return;
       }
        var h = helloGenerator();
        console.log(h.next());//{ value: 'hello', done: false }
        console.log(h.next());//{ value: 'generator', done: false }
        console.log(h.next());//{ value: 'undefined', done: true }

    var h = helloGenerator();仅仅是创建了这个函数的句柄,并没有实际执行,需要进一步调用next()才能触发方法。

   (1)创建了h对象,指向hellloGenerator  的句柄

    (2)第一次调用next() ,执行到"yield hello" ,暂缓执行,并返回了"hello"

    (3)第二次调用next(),继续上一次的执行,执行到"yield generator" ,暂缓执行,并返回了“generator”。

    (4)第三次调用next(),直接执行return,并返回done:true,表明结束

    经过上面的分析,yield实际就是暂缓执行的标示,每执行一次next(),相当于指针移动到下一个yield 位置

    Generator 函数是ES6 提供的一种异步编程解决方案。通过yield标识位和next()方法调用,实现函数的分段执行

     yield 只能配合Generator 函数使用,在普通函数中使用会报错。

    一句话:Generator 函数可以随心所欲的交出恢复函数的执行权,yield 交出执行权,next()恢复执行权

   应用场景: (1)协程(可以理解成多线程间的协作) (2)异步编程

 

4、运行以下代码片段时,控制台上会打印什么?

  

     var obj = { a: 1, b: 2 };
     Object.setPrototypeOf(obj, {c: 3});
     Object.defineProperty(obj, 'd', { value: 4, enumerable: false }); // what properties will be printed when we run the for-in loop?
     for(let prop in obj) { console.log(prop); }
 
  答案: a, b, c
 
 解析:for-in 循环遍历对象本身的可枚举属性以及对象从其原型继承的属性。可枚举属性是可以在for-in 循环期间包含和访问的属性
     
      1) Object.setPrototypeOf(obj,prototype)  // 将一个指定的对象的原型设置为另一个对象或者null  
       其中 obj 是将被设置原型的对象
       prototype  是 该对象新的原型
      2)Object.defineProperty(obj,prop,descriptor)
          obj: 需要定义属性的对象
          prop:需要定义的属性
         descriptor:属性的描述符
         返回值:返回此对象
    
     例子:
       let obj = Object.create(null);

       let descriptor = {
        configurable:false,
        writable:false,
       enumerable:false,
       value:'hello world'
      };
    Object.defineProperty(obj,'hello',descriptor);
   console.log(obj.hello);//hello world

 

5、 总结一下 object.assign()的用法 

 Object.assign 是ES6 新添加的接口,主要的用途是用来合并多个javascript 的对象

 Object.assign() 接受多个参数,第一个参数是目标对象,后面都是源对象,assign 方法将多个源对象的属性和方法都合并到了目标对象上面,

 如果出现同名属性(方法),后合并的属性(方法)会覆盖之前的同名属性(方法)。

 demo:

   var  target   =  {a:1}; // 目标对象

  var source1 = {b:2}; // 源对象 1

  var source1 = {c:3}; // 源对象 2

  var source1 = {c:4}; // 源对象 3

 Object.assign(target,source1,source2,source3);

 // 结果如下:

 { a:1;b:2;c:4}

 assign 接收的第一个参数应该是对象,如果不是对象的话,它会在内部转换成对象,所以如果碰到了null 或者 undefined 这种不能转换成对象的值的话,

 assign 就会报错。但是如果源对象的参数位置,接收到了无法转换为对象的参数的话,会忽略这个源对象参数.但是除了字符串会以数组形式拷贝入目标对象,其他值都不会产生效果

  demo: 

  const v1 = 'abc';

 const v2 = true;

 const v3 = 10;

const obj = Object.assign({},v1,v2,v3);

console.log(obj);  // {“0”:"a","1":"b","2":"c"}

 v1,v2,v3 分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性。

 Object.assign 拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举属性。

  属性名为Symbol 值的属性,也会被 Object.assign 拷贝

  Object.assign({a:'b'},{[ Symbol('c')] :'d'})

  // { a :'b' ,Symbol(c) : 'd'}

 

 注意点: 

 (1)浅拷贝

   Object.assign 方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,你们目标对象拷贝得到的是这个对象的引用

    const obj1 = { a:{b:1}};

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

  obj1.a.b = 2;

 obj2.a.b  // 2

 上面代码中,源对象obj1 的a 属性的值是一个对象,Object.assign 拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

(2)同名属性的替换 

   对于这种嵌套的对象,一旦遇到同名属性,Object.assign 的处理方法是替换,而不是添加。

(3)数组的处理

  Object.assign 可以用来处理数组,但是会把数组视为对象

 Object.assign([1,2,3],[4,5])   // [4,5,3]

 上面代码中,Object.assign 把数组视为属性名为 0、1、2 的对象,因此源数组的0号属性4覆盖了目标数组的0号属性1

 (4) 取值函数的处理

 Object.assign 只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制

   const source = {

    get foo() {return 1 }

  }

 const target = {};

 Object.assign(target,source)  // {foo : 1}

  上面代码中,source 对象的 foo 属性是一个取值函数,Object.assign 不会复制这个取值函数,只会拿到值后,将这个值复制过去。

 

常见用途

 Object.assign 方法有很多用处

 (1)为对象添加属性

   class Point {

    constructor(x,y){

     Object.assign(this,{x,y});

    }

  }

 通过 Objectt.assign 方法,将 x 属性和 y 属性添加到 Point 类的对象实例

 

(2)为对象添加方法

   Objetc.assiign(SomeClass.prototype,{

     someMtehod(arg1,arg2){

       ...

     },

     anotherMethod(){

      ...

    }

   })

  //  等同于下面的写法

  SomeClass.prototype.someMethood = function(arg1,arg2){...};

 SomeClass.prototype.anotherMethod = function(){...};

 上面代码使用了对象属性的简洁表示法,直接将两个方法放在大括号中,再使用 assign

 方法添加到 SomeClass.prototype 之中。

 

 (3)克隆对象

    function clone(origin){

      return Object.assign({},origin);

   }

 上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

 不过这种方法只能克隆原始对象自身的值,不能克隆它继承的值。

如果想要保持继承链,可以采用下面代码:

 function clone(origin){

  let originProto = Object.getPrototypeOf(origin);

 return Object.assign(Object.create(originProto),origin); 

}

如果源对象的属性中包含某个对象,也就是这个属性的值指向某个对象,像下面这样:

  var ob = {

   name:'obj name',

  content:{

    a:1,

    b:2

  }

 }

使用Object.assign ({},obj) 时,返回的目标对象中的content 属性与源对象obj 中 contetn 属性指向的同一块内存区域,即对obj 下的content 属性进行了浅拷贝。

 因此针对深拷贝,需要使用其他方法,可以使用 JSON.parse(JSON.stringify(obj)),因为Object.assign()拷贝的是属性值

 

 (4) 合并多个对象

  将多个对象合并到某个对象

  如果希望合并后返回一个新对象,可以对一个空对象合并

(5)为属性指定默认值

   const DEFAULTS = {

    logLevel:0,

   outputFormat:'html'

  }

function processContent(options){

  options = Object.assign({},DEFAULTS,options);

console.log(options);  

}

 

上面代码中。DEFAULTS 对象是默认值,options 对象是用户提供的参数。Object.assign 方法将DEFAULTS 和 options 合并成一个新对象,如果

两者有同名属性,则options 的属性值会覆盖DEFAULTS的属性值。

 

6、有关Promise 的 原理及用法

  1)Promise 出现的原因

   在Promise 出现以前,我们需要根据一个网络请求的结果,再去执行第二个网络请求,代码大概如下:

  请求1(function(请求结果1){

    请求2(function(请求结果2){

     处理请求结果 2

   })

 })

随着需求越来越复杂,回调地狱 出现了。

回调地狱带来的负面作用有以下几点:

   代码臃肿

  可读性差

  耦合度过高,可维护性差

 代码复用性差

 容易滋生bug

 只能在回调里处理异常

 代码逻辑书写顺序与执行顺序不一致,不利于阅读和维护

 异步操作的顺序变更时,需要大规模的代码重构

 回调函数基本都是匿名函数,bug 追踪困难

 这时,就有人思考了,能不能用一种更加友好的代码组织方式,解决异步嵌套的问题。

 于是Promise 规范诞生了,并且在业界有了很多实现来解决回调地狱的痛点。比如业界著名的Q 和 bluebird,bluebird 甚至号称运行最快的类库。

 

 2)什么是 Promise 

 Promise 是异步编程的一种解决方案,比传统的异步解决方案(回调函数) 和 事件 更合理、更强大。现已被ES6 纳入规范中。

对于几种常见异步编程方案:

  (1) 回调函数

  (2)事件监听

  (3)事件订阅

  (4)Promose 对象

  new Promise(请求1) .then(请求2(请求结果1)) .then(请求3(请求结果2)) .then(请求4(请求结果3)) .then(请求5(请求结果4)) .catch(处理异常(异常信息))

与回调式的写法相比,Promise 的写法更为直观,并且能够在外层捕获异步函数的异常信息。

    Promise 处理多个相关联的异步请求:

   (1)Promise  可以更直观的方式来解决"回调地狱"

       const request = url =>{

         return new Promise((resolve,reject)=>{

           $.get(url,data=>{

             resolve(data)

         })

       })

      }

   

     // 请求data1 request(url).then(data1 => { return request(data1.url); }).then(data2 => { return request(data2.url); }).then(data3 => { console.log(data3); }).catch(err => throw new Error(err));
 3) Promise 使用
 
   Promise 是一个构造函数,new Promsie 返回一个promise 对象,接收一个excutor 执行函数作为参数,excutor有两个函数类型形参 resolve reject
 
  const promise = new PRomise((resolve,reject)=>{
     // 异步处理
    // 处理结束后,调用 resolve 或 reject
  })
 
  4)promise相当于一个状态机
  
   promise 的三种状态
     
   pending
   fulfilled
   rejected
 
  (1)promise 对象初始化状态为pending
  (2)当调用resolve,会由pending=> fulfilled
  (3)当调用reject,会由pending => rejected
 
  5) promsie 对象方法
   
  (1)then 方法注册 当 resolve(成功)/ reject (失败)的回调函数 ( then 方法是异步执行的)
 
          promise.then(onFulfilled,onRejected)

   (2)resolve(成功) onFulfilled 会被调用

      const promise = new Promise((resolve, reject) => {

     resolve('fulfilled'); // 状态由 pending => fulfilled
    });
    promise.then(result => { // onFulfilled
    console.log(result); // 'fulfilled'
    }, reason => { // onRejected 不会被调用

    })

     (3)  reject(失败) onRejected 会被调用

  const promise = new Promise((resolve, reject) => {
  reject('rejected'); // 状态由 pending => rejected
  });
  promise.then(result => { // onFulfilled 不会被调用

  }, reason => { // onRejected
  console.log(reason); // 'rejected'
  })

  (4) promise.catch 

 

  promise.catch(onRejected)
  相当于
  promise.then(null, onRrejected);

  // 注意
  // onRejected 不能捕获当前onFulfilled中的异常
  promise.then(onFulfilled, onRrejected);

  // 可以写成:
  promise.then(onFulfilled)
  .catch(onRrejected);

    (5)promise chain

     promise.then 方法每次调用 都返回一个新的promise对象 所以可以链式写法。

  

  function taskA() {
  console.log("Task A");
  }
  function taskB() {
  console.log("Task B");
  }
  function onRejected(error) {
  console.log("Catch Error: A or B", error);
  }

  var promise = Promise.resolve();
  promise
  .then(taskA)
  .then(taskB)
  .catch(onRejected) // 捕获前面then方法中的异常

   (6) Promise 的静态方法 

     (1)Promise.resolve 返回一个fulfilled 状态的promise 对象

     (2)Promise.reject 返回一个reject状态的promise对象

     (3)Promise.all 接收一个promise对象数组为参数

       只有全部为 resolve 才会调用 ,通常会用来处理多个并行异步操作 

     (4)Promise.race 接收一个promise 对象数组为参数

        Promise.race 只要有一个promise 对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。

  7)Promise 相关题目(以下promise 均指代Promise 实例,环境是Node.js)

       题目一:

         

  const promise = new Promise((resolve, reject) => {
    console.log(1)
    resolve()
    console.log(2)
  })
  promise.then(() => {
    console.log(3)
  })
  console.log(4)   // 1 2 4 3

     解释: Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的。

 

       题目三:

       

  const promise = new Promise((resolve, reject) => {
    resolve('success1')
    reject('error')
    resolve('success2')
  })

  promise
  .then((res) => {
    console.log('then: ', res)
  })
  .catch((err) => {
    console.log('catch: ', err)
  })

      运行结果:  then:success1

      解释 构造函数中的resolve 或reject 只有第一次执行有效,多次调用没有任何作用(promise 状态一旦改变则不能再变)

   

    题目四:

      

  Promise.resolve(1)
  .then((res) => {
      console.log(res)
       return 2
  })
  .catch((err) => {
   return 3
  })
  .then((res) => {
      console.log(res)
   })

   运行结果: 1 2

   解释:promise 可以链式调用。提起链式调用我们通常会想到通过 return this 实现,不过Promise

 并不是这样实现的。promise 每次调用 .then  或者catch 都会返回一个新的promsie,从而实现了链式调用。

  题目五:

    

  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)
  })

    运行结果:

     once  

     success 1005

     success 1007

     解释: promise 的.then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说promise 内部状态一经改变,并且有了一值,

    那么后续每次调用.then 或者.catch 都会直接拿到该值。

  

  题目六:

   

  Promise.resolve()
     .then(() => {
    return new Error('error!!!')
      })
  .then((res) => {
       console.log('then: ', res)
  })
  .catch((err) => {
      console.log('catch: ', err)
  })

     运行结果:

    

        then: Error: error!!!
        at Promise.resolve.then (...)
        at ...

  解释: .then 或者.catch 中return 一个 error 对象 并不会抛出错误,所以不会被后续的.catch 捕获,需要改成其中一种:

      return Promise.reject(new Error("error!!!"))

      throw new Error('error!!!')

     因为返回任意一个非promise 的值都会被包裹成promise 对象,即 return new Error('error!!!')  等价于 return Promise.resolve(new Error(''error!!!))

   

  题目七:

    const promise = Promise.resolve()

              .then(()=>{

                 return promsie

              })

         promise.catch(console.error)

      运行结果:

     

  TypeError: Chaining cycle detected for promise #<Promise>
  at <anonymous>
  at process._tickCallback (internal/process/next_tick.js:188:7)
  at Function.Module.runMain (module.js:667:11)
  at startup (bootstrap_node.js:187:16)
  at bootstrap_node.js:607:3

     解释: .then 或 .catch 返回的值不能是promise 本身,否则会造成死循环。

 

   题目 八:

    Promise.resovle(1)

               .then(2)

               .then(Promise.resolve(3))

    .then(console.log)

     运行结果:

          1

    解释: .then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。

   

   题目九:

  

  Promise.resolve()
    .then(function success (res) {
    throw new Error('error')
   }, function fail1 (e) {
   console.error('fail1: ', e)
  })
  .catch(function fail2 (e) {
  console.error('fail2: ', e)
  })

  运行结果:

     fail2:Error:error

          at success (...)

          at ...

     解释:.then 可以接收两个参数,第一个是处理成功的函数,第二个是处理欧五的函数。.catch 是 .then 第二个参数的简便写法,但用法上有一点需要注意:

    .then 的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续.catch 可以捕获之前的错误。当然,以下代码也可以:

    

  Promise.resolve()
      .then(function success1 (res) {
      throw new Error('error')
  }, function fail1 (e) {
    console.error('fail1: ', e)
   })
  .then(function success2 (res) {
  }, function fail2 (e) {
    console.error('fail2: ', e)
  })

 

    题目十:

    

  process.nextTick(() => {
     console.log('nextTick')
  })
  Promise.resolve()
  .then(() => {
      console.log('then')
  })
  setImmediate(() => {
     console.log('setImmediate')
  })
  console.log('end')

    运行结果:

    end

    nextTick

   then 

   setImmediate

  解释: process.nextTick 和 promise.then 都属于microtask,而 setTmmediate 属于macrotask,在 事件循环的check

 阶段执行。事件循环的每个阶段之间都会执行microtask,事件循环的开始会先执行一次microtask

 

posted on 2019-07-23 10:02  艾薇儿802388  阅读(221)  评论(0)    收藏  举报