likes
comments
collection
share

JS面试题(二)

作者站长头像
站长
· 阅读数 17

异步

MDN:异步指两个或两个以上的对象或事件不同时存在或发生(或多个相关事物的发生无需等待其前一事物的完成)。

如何理解异步和同步

同步:同步是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行下去。

异步:异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有信息返回的时候会通知进程进行处理,这样就可以提高执行的效率了,即异步是我们发出的一个请求,该请求会在后台自动发出并获取数据,然后对数据进行处理,在此过程中,我们可以继续做其他操作,不管它怎么发出请求,不关心它怎么处理数据。

js是单线程语言,但是有时候需要等到某个时机才会执行某些操作,也就是异步操作。比如:

console.log('one');
setTimeout(() => {
    console.log('two');
    setTimeout(() => {
        console.log('three')
    }, 2000);
}, 2000);
console.log('six')
// one
// six
// two
// three

如果不断需要延时回调,就会出现一列回调小火箭,我们称之为回调地狱。虽然功能上不存在问题,但是难以维护。

promise

prmoise是一个对象,专门用于处理异步操作,该对象有三个状态:进行中(Pending)、已完成(Resolved)、已失败(Rejected),当前状态仅由异步操作的结果决定,不受任何其他操作控制,且一旦执行就无法取消。

Promise 有两个特点:

  • 对象状态不受外界影响;
  • 一旦状态改变了就不会再变。

也就是说,Promise任何时候都只有一种状态。

function timeout(data){
    return new Promise((resolve,reject) => {
        setTimeout(function() {
            if(data){
                resolve(data)
            }else{
                reject(data)
            }
        }, 2000);
    })
}

<!--resolve函数执行后,-->
<!--promise对象状态将会从“进行中”变为“已完成”,-->
<!--reject则是变为“已失败”,-->
<!--基于此我们可以使用then方法分别指定“成功”与“失败”状态的回调函数-->

timeout("hello").then(function(){
    console.log("resolve");
},function(){
    console.log("reject")
})
// resolve

timeout().then(function(){
    console.log("resolve");
},function(){
    console.log("reject")
})
// reject

Promise基本用法

可以通过Promise构造函数创建Promise对象,Promise构造函数接受两个参数:resolve和reject,由JS引擎提供。当Promise对象转移到成功的时候,调用resolve函数并将操作结果作为参数传递出去,当Promise对象状态变成失败时,reject函数将报出的错误作为参数传递出去。

只有Promise对象中调用resolve和reject的时候,才会执行then里边的内容。

function greet() {
  var promise = new Promise(function (resolve, reject) {
    var greet = "hello  world";
    resolve(greet);
  });
  return promise;
}
greet().then((v) => {
  console.log('resolve',v); 
},(s)=>{
  console.log('reject',s)
});

注意:创建一个Promise对象会立即执行里边的代码,所以为了更好控制代码运行的时刻,可以将其包含在一个函数中,并将这个promise返回。

Promise的then方法有三个参数:成功回调,失败回调,前进回调。一般情况下只实现第一个,后边的是可选的。

catch用法

function greet() {
  var promise = new Promise(function (resolve, reject) {
    var greet = "hello  world";
    reject(greet);
  });
  return promise;
}

greet()
  .then((v) => {
    console.log("resolve", v);
  })
  .catch(function () {
    console.log("catch");
  });

这个时候catch执行的是和reject一样的,也就是说如果Promise的状态变为reject时,会被catch捕捉到,不过需要特别注意的是如果前面设置了reject方法的回调函数,则catch不会捕捉到状态变为reject的情况。catch还有一点不同的是,如果在resolve或者reject发生错误的时候,会被catch捕捉到,这与java,c++的错误处理时一样的,这样就能避免程序卡死在回调函数中了。

promise的api

  • Promise.all()中的Promise序列会全部执行通过才认为是成功,否则认为是失败;
  • Promise.race()中的Promise序列中第一个执行完毕的是通过,则认为成功,如果第一个执行完毕的Promise是拒绝,则认为失败;
  • Promise.any()中的Promise序列只要有一个执行通过,则认为成功,如果全部拒绝,则认为失败;
  • Promise.allSettled()只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

实现符合 Promise/A+ 规范的 Promise

参考

Promise/A+规范

Promise/A+规范详情看这里

  1. promise 有三个状态:pending,fulfilled,or rejected;「规范 Promise/A+ 2.1」
  2. new promise时, 需要传递一个executor()执行器,执行器立即执行;
  3. executor接受两个参数,分别是resolve和reject;
  4. promise 的默认状态是 pending;
  5. promise 有一个value保存成功状态的值,可以是undefined/thenable/promise;「规范 Promise/A+ 1.3」
  6. promise 有一个reason保存失败状态的值;「规范 Promise/A+ 1.5」
  7. promise 只能从pending到rejected, 或者从pending到fulfilled,状态一旦确认,就不会再改变;
  8. promise 必须有一个then方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled, 和 promise 失败的回调 onRejected;「规范 Promise/A+ 2.2」
  9. 如果调用 then 时,promise 已经成功,则执行onFulfilled,参数是promise的value;
  10. 如果调用 then 时,promise 已经失败,那么执行onRejected, 参数是promise的reason;
  11. 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调onRejected;

基本的Promise

针对以上规范,我们大体上可以总结出如下promise实现的方案:

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class Promise {
    // 4.传入一个executor执行器
    constructor(executor) {
        // 1.存放状态
        this.status = 'pending';
        // 2.存放成功状态的值
        this.value = undefined;
        // 3.存放失败状态的值
        this.reason = undefined;

        // 6.为executor提供resolve函数
        let resolve = (value) => {
            // 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resovle/reject 方法
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value
            }
        }

        // 7.为executor提供reject函数
        let reject = (reason) => {
            // 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resovle/reject 方法
            if (this.status === PENDING) {
                this.status === REJECTED;
                this.reason = reason
            }
        }

        try {
            // 5.立即执行executor,将resolve和reject传递给使用者
            executor(resolve, reject)
        } catch (error) {
            // 发生异常时执行失败逻辑
            reject(error)
        }
    }
    // 8.promise必须有then方法,调用成功和失败时对应的方法
    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value)
        }
        if (this.status === REJECTED) {
            onRejected(this.reason)
        }
    }
}

简单测试一下:

// test
const promise = new Promise((resolve, reject) => {
    resolve('成功');
}).then(
    (data) => {
        console.log('success', data)
    },
    (err) => {
        console.log('faild', err)
    }
)
// 控制台输出--->success 成功

如果executor是异步操作呢:

const promise = new Promise((resolve, reject) => {
    // 传入一个异步操作
    setTimeout(() => {
        resolve('成功');
    }, 1000);
}).then(
    (data) => {e
        console.log('success', data)
    },
    (err) => {
        console.log('faild', err)
    }
)

控制台什么都没有打印。原因是当调用then方法时,promise还处于pending状态,所以并没有执行onFulfilled或者onRejected。

所以,我们应当在执行then函数时,将成功或者失败的回调函数存储起来,在executor()的异步任务被执行时,触发 resolve 或 reject,依次调用成功或失败的回调。

满足异步的Promise

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class Promise {
    // 4.传入一个executor执行器
    constructor(executor) {
        // 1.存放状态
        this.status = 'pending';
        // 2.存放成功状态的值
        this.value = undefined;
        // 3.存放失败状态的值
        this.reason = undefined;

        this.onResolvedCallbacks = []
        this.onRejectedCallbacks = []

        // 6.为executor提供resolve函数
        let resolve = (value) => {
            // 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resovle/reject 方法
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }

        // 7.为executor提供reject函数
        let reject = (reason) => {
            // 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resovle/reject 方法
            if (this.status === PENDING) {
                this.status === REJECTED;
                this.reason = reason;
                this.onRejectedCallbacks.forEach(fn => fn())
            }
        }

        try {
            // 5.立即执行executor,将resolve和reject传递给使用者
            executor(resolve, reject)
        } catch (error) {
            // 发生异常时执行失败逻辑
            reject(error)
        }
    }
    // 8.promise必须有then方法,调用成功和失败时对应的方法
    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value)
        }
        if (this.status === REJECTED) {
            onRejected(this.reason)
        }
        if (this.status === PENDING) {
            // 如果promise的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行
            this.onResolvedCallbacks.push(() => {
                onFulfilled(this.value)
            });

            // 如果promise的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行
            this.onRejectedCallbacks.push(() => {
                onRejected(this.reason);
            })
        }
    }
}

测试一下:


// test

const promise = new Promise((resolve, reject) => {
    // 传入一个异步操作
    setTimeout(() => {
        resolve('成功');
    }, 3000);
}).then(
    (data) => {
        console.log('success', data)
    },
    (err) => {
        console.log('faild', err)
    }
)

三秒之后控制台打印内容:success 成功

then 的链式调用&值穿透特性

在我们使用 Promise 的时候,当 then 函数中 return 了一个值,不管是什么值,我们都能在下一个 then 中获取到,这就是所谓的then 的链式调用。而且,当我们不在 then 中放入参数,例:promise.then().then(),那么其后面的 then 依旧可以得到之前 then 返回的值,这就是所谓的值的穿透

for example: 链式调用:

const promise = new Promise((resolve, reject) => {
    resolve('promise resolve')
}).then(
    (data) => {
        console.log('success', data)
        return '第一个then resolve'
    },
    (err) => {
        console.log('faild', err)
    }
).then(res=>{
    console.log(res)
})
// success promise resolve
// 第一个then resolve

穿透性:

const promise = new Promise((resolve, reject) => {
    // 传入一个异步操作
    resolve('promise resolve')
}).then()
    .then(res => {
        console.log(res)
    })
// promise resolve

那具体如何实现链式调用和穿透性呢?简单思考一下,如果每次调用 then 的时候,我们都重新创建一个 promise 对象,并把上一个 then 的返回结果传给这个新的 promise 的 then 方法,不就可以一直 then 下去了么?

结合 Promise/A+ 规范梳理一下思路:

  1. then 的参数 onFulfilled 和 onRejected 可以缺省,如果 onFulfilled 或者 onRejected不是函数,将其忽略,且依旧可以在下面的 then 中获取到之前返回的值;「规范 Promise/A+ 2.2.1、2.2.1.1、2.2.1.2」
  2. promise 可以 then 多次,每次执行完 promise.then 方法后返回的都是一个“新的promise";「规范 Promise/A+ 2.2.7」
  3. 如果 then 的返回值 x 是一个普通值,那么就会把这个结果作为参数,传递给下一个 then 的成功的回调中;
  4. 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调中;「规范 Promise/A+ 2.2.7.2」
  5. 如果 then 的返回值 x 是一个 promise,那么会等这个 promise 执行完,promise 如果成功,就走下一个 then 的成功;如果失败,就走下一个 then 的失败;如果抛出异常,就走下一个 then 的失败;「规范 Promise/A+ 2.2.7.3、2.2.7.4」
  6. 如果 then 的返回值 x 和 promise 是同一个引用对象,造成循环引用,则抛出异常,把异常传递给下一个 then 的失败的回调中;「规范 Promise/A+ 2.3.1」
  7. 如果 then 的返回值 x 是一个 promise,且 x 同时调用 resolve 函数和 reject 函数,则第一次调用优先,其他所有调用被忽略;「规范 Promise/A+ 2.3.3.3.3」

async

promise对象的链式串联方式虽然解决了回调函数的层层嵌套问题,但是then操作过多过长时还是会出现冗余臃肿的情况,此时可以使用async函数。

function timeout(data){
    //async 和 promise 结合使用 
    return new Promise((resolve,reject) => { 
        setTimeout(function(){
            if(data){
                resolve(data);
            }else{
                reject(data);
            }
        }, 2000);
    })
}

async function run(){
    console.log('aget run start');
    await timeout('agent');
    console.log('agent run end');
} 

console.log('no_await');

run();

// no_await
// aget run start
// agent run end

async 函数是 Generator 函数的语法糖。使用 关键字 async 来表示,在函数内部使用 await来表示异步,await关键字只能用在async定义的函数内。async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await。比Generator更语义化。

相对于promise的优势

  • 语法简洁,同步代码执行异步操作。
  • 错误处理

promise中,try/catch不能处理promise内部的错误,因为promise对象抛出的错误不会传递到外层。所以捕获异常需要使用.catch,这样错误处理代码非常冗余。并且,在我们的实际生产代码会更加复杂。

//promise捕获错误
const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        // JSON.parse可能会出错
        const data = JSON.parse(result)
        console.log(data)
      })
      // 处理异步代码的错误
      .catch((err) => {
        console.log(err)
      })
  } catch (err) { //此处catch无效
    console.log(err)
  }
}
 
//async捕获错误
const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}

这里有一点需要注意,当 async 函数中只要一个 await 出现 reject 状态,则后面的 await 都不会被执行。所以要把第一个await放在try…catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。如果有多个await命令,可以统一放在try…catch结构中

async function main() {
  try {
    const val1 = await fetchDataA();
    const val2 = await fetchDataB();
    const val3 = await fetchDataC();
  }
  catch (err) {
    console.error(err);
  }
}

async/await 的执行顺序

async返回什么

先看看看以下代码的输出值:

async function asyncReturn() {
     return 'async返回的是什么?'
}

var result = asyncReturn();
console.log(result); //Promise { 'async返回的是什么?' }

从结果中可以看到async函数返回的是一个promise对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve()封装成 Promise 对象。

如果async函数没有返回值:

async function testAsync1() {
    console.log("hello async");
}
let result1 = testAsync1();
console.log(result1); //Promise {<fulfilled>: undefined}
await做了什么处理

从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。

很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈的代码。等本轮事件循环执行完了之后又会跳回到async函数中等待await后面表达式的返回值,如果返回值为非promise则继续执行async函数后面的代码,否则将返回的promise放入promise队列(Promise的Job Queue)。

function testSometing() {
    console.log("2-执行testSometing");  //3--> 打印2-执行testSometing
    return "5-testSometing";            //4--> 返回值,await让出线程,向下执行
}

async function testAsync() {
    console.log("6-执行testAsync");     //11-->打印6-执行testAsync
    return Promise.resolve("8-hello async"); //12-->返回值,await让出线程,执行别的代码(promise)
}

async function test() {                 //整体从调用test开始
    console.log("1-test start...");     //1-->打印1-test start...
    const v1 = await testSometing();    //2-->执行到这去执行testSometing()
    console.log(v1);                    //9-->打印v1:5-testSometing
    const v2 = await testAsync();       //10-->执行testAsync()
    console.log(v2);                    //14-->打印8-hello async
    console.log(v1, v2);                //15-->打印5-testSometing,8-hello async,结束
}


test();

var promise = new Promise((resolve)=> { //5-->执行promise
console.log("3-promise start..");       //6--> 打印3-promise start..
resolve("7-promise");});//关键点2       //7-->将promise放入promise的队列
promise.then((val)=> console.log(val)); //13-->打印7-promise,返回test()

console.log("4-test end...")            //8-->打印4-test end... 本轮事件循环执行结束,跳回到async函数test()中。

加上seTimeout看看结果如何:

async function async1() {
    console.log("async1 start");
    await async2();
    console.log("async1 end");
    await async3()
    await async4()
}

async function async3() {
    console.log("async3")
}

async function async4() {
    console.log('async4')
}

async function async2() {
    console.log('async2');
}

console.log("script start");

setTimeout(function () {
    console.log("settimeout");
}, 0);

async1();

new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});

console.log('script end');   
/*
script start
async1 start
async2
promise1
script end
async1 end
async3
promise2
async4
settimeout*/

再看一个例子:

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1

输出结果:
1 1
2 10
3 20

解析:

  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generators ,generators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,遇到await就会立即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • 然后后面就是常规执行代码了

generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同

特征:

  • function 命令与函数名之间有一个星号
  • 函数体内部使用 yield 语句定义不同的内部状态
function *f(){
    yield "hello";
    yield "world";
    return "!"
}

let fg=f();

  // 调用遍历器对象的 next 方法,使得指针移向下一个状态,直到遇到下一条 yield 语句(或 return 语句)为止
  // done 为 true 时遍历结束
 
console.log(fg.next());
console.log(fg.next());
console.log(fg.next());
// { value: 'hello', done: false }
// { value: 'world', done: false }
// { value: '!', done: true }

模块化

我们都知道在早期JavaScript模块这一概念,都是通过script标签引入js文件代码。当然这写基本简单需求没有什么问题,但当我们的项目越来越庞大时,我们引入的js文件就会越多,这时就会出现以下问题:

  • js文件作用域都是顶层,这会造成变量污染
  • js文件多,变得不好维护
  • js文件依赖问题,稍微不注意顺序引入错,代码全报错

为了解决以上问题JavaScript社区出现了CommonJs,CommonJs是一种模块化的规范,包括现在的NodeJs里面也采用了部分CommonJs语法在里面。那么在后来Es6版本正式加入了Es Module模块,这两种都是解决上面问题,那么都是解决什么问题呢。

  • 解决变量污染问题,每个文件都是独立的作用域,所以不存在变量污染
  • 解决代码维护问题,一个文件里代码非常清晰
  • 解决文件依赖问题,一个文件里可以清楚的看到依赖了那些其它文件

CommonJS

CommonJS的一个模块就是一个脚本文件,通过执行该文件来加载模块。CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,他的exports属性(即module.exports)是对外接口。加载某个模块,其实就是加载该模块的module.exports属性。

我们见过这样模块引用:

var myModule = require('module');
myModule.sayHello();

这是因为我们把模块的方法定义在了模块的属性上:

//module.js
module.exports.sayHello = function(){
    console.log('Hello');
}

还可以换另外一种形式:

module.exports = sayHello;
//调用
var sayHello = require('module');
sayHello();

require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象(模块可以多次加载,但是在第一次加载时才会运行,结果被缓存),这个结果长这样:


{
    id:'...',
    exports:{...},
    loaded:true,
    ...
}

常用形式:

//myModel.js
var name = 'Byron';  
  
function printName(){  
    console.log(name);  
}  
  
function printFullName(firstName){  
    console.log(firstName + name);  
}  
  
module.exports = {  
    printName: printName,  
    printFullName: printFullName
}

//引用
var nameModule = require('./myModel.js');
nameModule.printName();

Node.js的模块机制实现就是参照CommonJS的标准。但是Node.js额外做了一件事,即为每个模块提供了一个exports变量,以指向module.exports,这相当于在每个模块最开始,写这么一行代码:

var exports = module.exports;

CommonJS的特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

ES Modules

ES Modules 的模块化能力由 export 和 import 组成,export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。我们可以这样定义一个模块:

// 第一种方式
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

// 第二种方式
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

然后再这样引入他们:

import { firstName, lastName, year } from 'module';
import { firstName as newName } from 'module';
import * as moduleA from 'module';

我们常用的方式:

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'

CommonJs和Es Module的区别

CommonJs

  • CommonJs可以动态加载语句,代码发生在运行时
  • CommonJs混合导出,还是一种语法,只不过不用声明前面对象而已,当我导出引用对象时之前的导出就被覆盖了
  • CommonJs导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查引起变量污染

Es Module

  • Es Module是静态的,不可以动态加载语句,只能声明在该文件的最顶部,代码发生在编译时
  • Es Module混合导出,单个导出,默认导出,完全互不影响
  • Es Module导出是引用值之前都存在映射关系,并且值都是可读的,不能修改

参考链接

防抖和节流

防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的情况会每隔一定时间(参数wait)调用函数。

防抖技术即是可以把多个顺序的调用合并成一次,也就是在一定时间内,控制事件被触发的次数。比如,500ms内没有连续触发两次scroll事件时才会触发函数,即一直滚动过程中不会一直触发,只有停顿500ms时才可以触发。

但是防抖函数也存在问题。比如我们希望下滑过程中图片被不断加载出来,而不是停下时才被加载。又或者下滑时候数据的ajax请求也是同理。这类场景我们就会用到另一种技巧,节流函数。

节流函数允许一个函数在X秒内执行一次。与防抖相比多了一个mustRun属性,代表mustRun毫秒内,必然会触发一次函数,大概功能就是如果在一段时间内 scroll 触发的间隔一直短于 500ms ,那么能保证事件我们希望调用的 handler 至少在 1000ms 内会触发一次。

防抖

<!DOCTYPE html>
<html>
<head> 
<meta charset="utf-8"> 
<title>Debouncing 防抖</title> 
</head>
<body>
<div style="width: 100%;height: 500px;background-color: aqua;">div1</div>
<div style="width: 100%;height: 500px;background-color: brown;">div2</div>
<script>
    function debounce(func,wait){
        var timeout;
        return function(){
            clearTimeout(timeout);
            timeout=setTimeout(func,wait);
        }
    };

    function realFunc(){
        console.log('success!!!!!!!')
    };
    // 采用了防抖
    window.addEventListener('scroll',debounce(realFunc,500));//500ms内没有连续触发两次scroll事件,才会真正触发我们真正想在scroll事件中触发的函数。
    // 没采用防抖
    window.addEventListener('scroll',realFunc);//只要是出发了scroll就打印
</script>
</body>
</html>

防抖技术即是可以把多个顺序的调用合并成一次,也就是在一定时间内,控制事件被触发的次数。上边例子中,500ms内没有连续触发两次scroll事件时才会触发函数,即一直滚动过程中不会一直触发,只有停顿500ms时才可以触发。

但是防抖函数也存在问题。比如我们希望下滑过程中图片被不断加载出来,而不是停下时才被加载。又或者下滑时候数据的ajax请求也是同理。这类场景我们就会用到另一种技巧,节流函数。

节流

节流函数允许一个函数在X秒内执行一次。与防抖相比多了一个mustRun属性,代表mustRun毫秒内,必然会触发一次函数,看简单实例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>throttling 节流</title>
</head>

<body>
    <div style="width: 100%;height: 5000px;background-color: aqua;">div1</div>
    <div style="width: 100%;height: 5000px;background-color: brown;">div2</div>
    <script>
        function throrttle(func, wait, mustRun) {
            var timeout,
                startTime = new Date();

            return function () {
                var context = this,
                    args = arguments,
                    curTime = new Date();

                clearTimeout(timeout);
                if (curTime - startTime >= mustRun) {
                    func.apply(context, args);
                    startTime = curTime;
                } else {
                    timeout = setTimeout(func, wait);
                }
            }
        };
        function realFunc(){
            console.log('success!!!');
        };
        window.addEventListener('scroll',throrttle(realFunc,500,1000));
    </script>
</body>

</html>

大概功能就是如果在一段时间内 scroll 触发的间隔一直短于 500ms ,那么能保证事件我们希望调用的 handler 至少在 1000ms 内会触发一次。

map方法

map 方法会给原数组中的每个元素都按顺序调用一次callback 函数。callback 每次执行后的返回值(包括 undefined)组合起来形成一个新数组。 callback 函数只会在有值的索引上被调用;那些从来没被赋过值或者使用 delete 删除的索引则不会被调用。

var arr = [1,2,3];
var new_arr = arr.map(e => e*2);
console.log(new_arr); //[ 2, 4, 6 ]

var another_arr =arr.forEach(e => e*3)
console.log(another_arr) //undefined
console.log(arr)//[ 1, 2, 3 ]

map生成一个新数组,当你不打算使用返回的新数组却使用map是违背设计初衷的,请用forEach或者for-of替代。

不该使用map:

  • 你不打算使用返回的新数组
  • 你没有从回调函数中返回值。
callback 函数会被自动传入三个参数:数组元素,元素索引,原数组本身。

var arr = [1,2,3];
var map_canshu = arr.map((a,b,c) => {
    console.log('a-->',a);
    console.log('b-->',b);
    console.log('c-->',c);
})

/*
a--> 1
b--> 0
c--> [ 1, 2, 3 ]
a--> 2
b--> 1
c--> [ 1, 2, 3 ]
a--> 3
b--> 2
c--> [ 1, 2, 3 ]
*/

flatMap方法

FlatMap 和 map 的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。

var arr1 = [1, 2, 3, 4];

arr1.map(x => [x * 2]); 
// [[2], [4], [6], [8]]

arr1.flatMap(x => [x * 2]);
// [2, 4, 6, 8]

目前该函数在浏览器中还不支持。

reduce方法

reduce()方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

var redArr = [1,2,3,4];
var reducer = (accumulator, currentValue) => accumulator + currentValue;

console.log(redArr.reduce(reducer));  //10

reducer 函数接收4个参数:

  • Accumulator (acc) (累计器)
  • Current Value (cur) (当前值)
  • Current Index (idx) (当前索引)
  • Source Array (src) (源数组)

reducer函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值

proxy

什么是Proxy

Proxy 也就是代理,可以帮助我们完成很多事情,例如对数据的处理,对构造函数的处理,对数据的验证,说白了,就是在我们访问对象前添加了一层拦截,可以过滤很多操作,而这些过滤,由你来定义。

语法

let p = new Proxy(target , handler)
  • target :需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

  • handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器)。

方法

handler.get()

该方法用于拦截对象的读取属性操作。

var p = new Proxy(target, {
  get: function(target, property, receiver) {
  }
});

参数:

  • target:目标对象。
  • property:被获取的属性名。
  • receiver:Proxy或者继承Proxy的对象
  • 返回值:可以返回任意值

约束:如果违背了以下的约束,proxy会抛出 TypeError:

  • 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同。
  • 如果要访问的目标属性没有配置访问方法,即get方法是undefined的,则返回值必须为undefined。

示例:

var p = new Proxy({}, {
  get: function(target, prop, receiver) {
    console.log("called: " + prop);
    return 10;
  }
});

console.log(p.a); // "called: a"

违反约束情况:

var obj = {};

Object.defineProperty(obj, "a", { 
  configurable: false, 
  enumerable: false, 
  value: 10, 
  writable: false 
});

var p = new Proxy(obj, {
  get: function(target, prop) {
    return 20;
  }
});

p.a; //会抛出TypeError

handler.set()

该方法用于拦截设置属性值的操作。

语法:

 new Proxy(target, {
  set: function(target, property, value, receiver) {
  }
});

参数:

  • target:目标对象。
  • property:被设置的属性名。
  • value:被设置的新值。
  • receiver:最初被调用的对象。通常是proxy本身,但handler的set方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是proxy本身)。比如,假设有一段代码执行 obj.name ="jen",obj不是一个proxy且自身不含name属性,但它的原型链上有一个proxy,那么那个proxy的set拦截函数会被调用,此时obj会作为receiver参数传进来。
  • 返回值:set方法应该返回一个布尔值,返回true代表此次设置属性成功了,如果返回false且设置属性操作发生在严格模式下,那么会抛出一个TypeError。

约束:如果违背以下的约束条件,proxy会抛出一个TypeError:

  • 若目标属性是不可写及不可配置的,则不能改变它的值。
  • 如果目标属性没有配置存储方法,即set方法是undefined的,则不能设置它的值。
  • 在严格模式下,若set方法返回false,则会抛出一个 TypeError 异常。

示例:

var p = new Proxy({}, {
  set: function(target, prop, value, receiver) {
    target[prop] = value;
    console.log('property set: ' + prop + ' = ' + value);
    return true;
  }
})

console.log('a' in p);  // false

p.a = 10;               // "property set: a = 10"
console.log('a' in p);  // true
console.log(p.a);       // 10

基础事例

var handler = {
    get:function(target,name){
    return name in target ? target[name] : 37;
    }
};

var p = new Proxy({},handler);
p.a = 1;
p.b = undefined;
console.log(p.a,p.b); //1 undefined
console.log('c' in p, p.c); //false 37

无操作转发代理

在以下例子中,我们使用了一个原生 JavaScript对象,代理会将所有应用到它的操作转发到这个对象上。

let target = {};
let p = new Proxy(target, {});

p.a = 37;   // 操作转发到目标

console.log(target.a);    // 37. 操作已经被正确地转发

代理p将所有应用于他的操作转发到target

验证

通过代理,你可以轻松地验证向一个对象的传值。这个例子使用了set。

let handler = {
    set: function (obj, prop, value) {
        if (prop === 'age') {
            if (!Number.isInteger(value)) {
                throw new TypeError('The age is not an integer');
            }
            if (value > 200) {
                throw new RangeError('The age seems invalid');
            }
        }

        // The default behavior to store the value
        obj[prop] = value;

        // 表示成功
        return true;
    }
};

        let person = new Proxy({}, handler);

        person.age = 100;

        console.log(person.age);
        // 100

        person.age = 'young';
        // 抛出异常: Uncaught TypeError: The age is not an integer

        person.age = 300;
        // 抛出异常: Uncaught RangeError: The age seems invalid

上边示例相当于,每当向person里边添加属性时,会拦截操作,判断属性值是否满足要求,满足要求就存起来,否则就抛出异常。

0.1+0.2!==0.3

先来个示例:

console.log(0.1 + 0.2 == 0.3) //false
console.log(0.1 + 0.2 > 0.3) //true

原因在于在JS中采用的IEEE 754的双精度标准,将数字存储为双精度浮点数,计算机内部存储数据的编码的时候,0.1在计算机内部根本就不是精确的0.1,而是一个有舍入误差的0.1。当代码被编译或解释后,0.1已经被四舍五入成一个与之很接近的计算机内部数字,以至于计算还没开始,一个很小的舍入错误就已经产生了。这也就是 0.1 + 0.2 不等于0.3 的原因。

另外要注意,不是所有浮点数都有舍入误差。二进制能精确地表示位数有限且分母是2的倍数的小数,比如0.5,0.5在计算机内部就没有舍入误差。所以0.5 + 0.5 === 1

如何解决这类问题

用整数表示

最好的方法就是我们想办法规避掉这类小数计算时的精度问题就好了,那么最常用的方法就是将浮点数转化成整数计算。因为整数都是可以精确表示的。通常的解决办法 就是 把计算数字提升 10 的N次方倍,再除以 10的N次方。一般都用 1000 就行了。

console.log((0.1*1000 + 0.2 * 1000)/1000 == 0.3) //true

bignumber.js

bignumber.js会在一定精度内,让浮点数计算结果符合我们的期望。

{
  let x = new BigNumber(0.1);
  let y = new BigNumber(0.2)
  let z = new BigNumber(0.3)

  console.log(z.equals(x.add(y))) // 0.3 === 0.1 + 0.2, true
  console.log(z.minus(x).equals(y)) // true
  console.log(z.minus(y).equals(x)) // true
}

原生

parseFloat((0.1 + 0.2).toFixed(10));
console.log(parseFloat((0.1 + 0.2).toFixed(10))) //0.3

script标签中的defer和async

(1)defer属性规定是否延迟执行脚本,直到页面加载为止。async属性规定脚本一旦可用,就异步执行。

(2)defer并行加载JavaScript文件,会按照页面上script标签的顺序执行。async并行加载JavaScript文件,下载完成立即执行,不会按照页面上script标签的顺序执行。

具体如下图:

JS面试题(二)

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

也就是说async是乱序的,而defer是顺序执行,这也就决定了async比较适用于百度分析或者谷歌分析这类不依赖其他脚本的库。从图中可以看到一个普通的<script> 标签的加载和解析都是同步的,会阻塞DOM的渲染,这也就是我们经常会把<script>写在<body>底部的原因之一,为了防止加载资源而导致的长时间的白屏,另一个原因是js可能会进行DOM操作,所以要在DOM全部渲染完后再执行。

js和css是怎样阻塞DOM解析和页面渲染的

参考链接

css不会阻塞DOM的解析,会阻塞页面渲染

浏览器是解析DOM生成DOM Tree,结合CSS生成的CSS Tree,最终组成render tree,再渲染页面。由此可见,在此过程中CSS完全无法影响DOM Tree,因而无需阻塞DOM解析。然而,DOM Tree和CSS Tree会组合成render tree。所以也可以得到,css是会阻塞页面渲染的。

看接下来代码:

<header>
    <link rel="stylesheet" href="/css/sleep3000-common.css">
    <script src="/js/logDiv.js"></script>
</header>

答案是浏览器会转圈圈三秒,但此过程中不会打印任何东西,之后呈现出一个浅蓝色的div(common.css),再打印出null。结果好像CSS不单阻塞了页面渲染,还阻塞了DOM 的解析。

其实阻塞DOM解析的是js,如果js脚本中要获取元素的样式,宽高等css控制的属性,浏览器是需要计算的,也就是依赖于css的,只好等所有的样式加载完了之后再执行js。

<script><link>同时在头部的话,<script>在上可能会更好,之所以是可能,是因为如果<link>的内容下载更快的话,是没影响的,但反过来的话,JS就要等待了,然而这些等待的时间是完全不必要的。

js阻塞DOM的解析和页面的渲染

浏览器并不知道脚本的内容是什么,如果先行解析下面的DOM,万一脚本内全删了后面的DOM,浏览器就白干活了。更别谈丧心病狂的document.write。浏览器无法预估里面的内容,那就干脆全部停住,等脚本执行完再干活就好了。

对此的优化其实也很显而易见,具体分为两类:

  • 如果JS文件体积太大,同时你确定没必要阻塞DOM解析的话,不妨按需要加上defer或者async属性,此时脚本下载的过程中是不会阻塞DOM解析的。
  • 如果是文件执行时间太长,不妨分拆一下代码,不用立即执行的代码,可以使用一下以前的黑科技:setTimeout()。当然,现代的浏览器很聪明,它会“偷看”之后的DOM内容,碰到如<link>、<script>和<img>等标签时,它会帮助我们先行下载里面的资源,不会傻等到解析到那里时才下载。

结论:

  • CSS 不会阻塞 DOM 的解析,但会阻塞页面渲染。
  • JS 阻塞 DOM 解析,但浏览器会”偷看”DOM,预先下载相关资源。
  • 浏览器遇到 <script>且没有defer或async属性的 标签时,会触发页面渲染(浏览器不知道脚本的内容,因而碰到脚本时,只好先渲染页面,确保脚本能获取到最新的DOM元素信息,尽管脚本可能不需要这些信息),因而如果前面CSS资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本。
  • <script>最好放底部,<link>最好放头部,如果头部同时有<script><link>的情况下,最好将<script>放在<link>上面

嵌入js和外部js的差别

内部js会阻塞整个DOM的解析,外部js只会阻塞其后边js的解析:

<!--index.js-->
var count = 0;
for (var i = 0; i < 100000; i++) {
    for (var j = 0; j < 10000; j++) {
        count++;
    }
}
console.log(count);

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
</head>

<body>
    <div>div1</div>
    <script src="./index.js"></script>
    <div>div2</div>
    <div>div3</div>
</body>

</html>
<!-- 出现浅绿色div1,之后转圈,打印100000,出现浅绿色div1,div2 -->
<!-- ------------------------------------------------------ -->

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
</head>

<body>
    <div>div1</div>
    <script>
        var count = 0;
        for (var i = 0; i < 100000; i++) {
            for (var j = 0; j < 10000; j++) {
                count++;
            }
        }
        console.log(count);
    </script>
    <div>div2</div>
    <div>div3</div>
</body>

</html> 
<!-- 转两圈之后,打印100000,出现浅绿色div1,div2,div3-----嵌入的js阻塞整个DOM解析 -->
<!-- ------------------------------------------------------------------ -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script defer src="./index.js"></script>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
</head>
<body>
    <div>div1</div>
</body>
</html> 
<!-- 加了defer之后:先出现浅绿色div,在打印100000----defer将及时执行延时到了DOM加载完成时(并未延时下载) -->
<!-- ---------------------------------------------------------------------- -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="./index.js"></script>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
</head>
<body>
    <div>div1</div>
</body>
</html>
<!-- 转两圈,打印100000,出现浅绿色div---js阻塞DOM解析和渲染 -->

encodeURL和decodeURL

encodeURL()用于将URL转换为十六进制编码

decodeURL()用于将编码的URL转换为正常的URL

浏览器多个标签页之间的通信

调用localStorage

在一个标签页里面使用 localStorage.setItem(key,value)添加(修改、删除)内容;

在另一个标签页里面监听 storage 事件。即可得到 localstorge 存储的值,实现不同标签页之间的通信。

<!--index1.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style></style>
</head>

<body>
    <input type="button" id="btn" value="提交">
    <script>
        var bt = document.getElementById('btn')
        bt.onclick = function () {
            localStorage.setItem("name", 'I am the value of name');
        };
    </script>
</body>

</html>
<!-- index2.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style></style>
</head>

<body>
    <script>
        window.addEventListener("storage", function (event) {
            console.log(event.key + "=" + event.newValue);
        });
    </script>
</body>

</html>

调用cookie + setInterval()

将要传递的信息存储在cookie中,每隔一定时间读取cookie信息,即可随时获取要传递的信息。

<!--index1.html-->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style></style>
</head>

<body>
    <input type="button" id="btn" value="提交">
    <script>
        var bt = document.getElementById('btn')
        bt.onclick = function () {
            document.cookie="username=John Doe";
        };
    </script>
</body>

</html>
<!--index2.html-->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style></style>
</head>

<body>
    <script>
        function getCookie(key) {
           return document.cookie;
        }
        setInterval(function () {
            console.log("name=" + getCookie("name"));
        }, 10000);    
    </script>
</body>

</html>

null 和undefined

首先看一个判断题:null和undefined 是否相等

console.log(null==undefined)//true
console.log(null===undefined)//false

观察可以发现:null和undefined 两者相等,但是当两者做全等比较时,两者又不等。

null类型,代表“空值”,代表一个空对象指针,使用typeof运算得到object,可以认为它是一个特殊的对象值。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。是不应该有值。

undefined类型,当一个声明了一个变量未初始化时,得到的就是undefined。是应该有值,但是尚未赋值

null表示”没有对象”,即该处不应该有值。典型用法是:

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。

undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。典型用法是:

  • 变量被声明了,但没有赋值时,就等于undefined。
  • 调用函数时,应该提供的参数没有提供,该参数等于undefined。
  • 对象没有赋值的属性,该属性的值为undefined。
  • 函数没有返回值时,默认返回undefined。

JavaScript中不同类型的错误有几种

Load time errors :该错误发生于加载网页时,例如出现语法错误等状况,称为加载时间错误,并且会动态生成错误。

Run time errors :由于在HTML语言中滥用命令而导致的错误。

Logical Errors :这是由于在具有不同操作的函数上执行了错误逻辑而发生的错误。

  1. EvalError - Eval错误 该对象表示全局函数 eval()中发生的错误。可以通过构造函数创建这个对象的实例。

  2. ReferenceError-引用错误 该对象会在引用未定义的变量时触发,也可以通过构造函数创建这个对象的实例。

  3. RangeError-范围错误 该错误对象会在值超过有效范围时触发,也可以通过构造函数创建这个对象的实例。

  4. SyntaxError-语法错误 该错误对象在使用不合法的语法结构时触发,也可以通过构造函数创建这个对象的实例。

  5. TypeError-类型错误 该对象会在对象用来表示值的类型非预期类型时触发,也可以通过构造函数创建这个对象的实例。

  6. URIError-URI错误 该错误会在错误使用全局URI函数 如encodeURI()、decodeURI()等时触发。也可以通过构造函数创建该对象的实例。

为什么会出现ES6新特性

每一次标准的诞生都意味着语言的完善,功能的加强。JavaScript语言本身也有一些令人不满意的地方。

  • ES6的目标,是使得JavaScript语言可以用来编写大型的复杂的应用程序,成为企业级开发语言。

  • 变量提升特性增加了程序运行时的不可预测性

  • 语法过于松散,实现相同的功能,不同的人可能会写出不同的代码

set和map区别

  1. 初始化需要的值不一样,Map需要的是一个二维数组,而Set 需要的是一维 Array 数组
  2. Map 是键值对的存在,值不作为健;而 Set 没有 value 只有 key,value 就是 key;
  3. Map 和 Set 都不允许键重复
  4. Map的键是不能修改,但是键对应的值是可以修改的;Set不能通过迭代器来改变Set的值,因为Set的值就是键。

基本数据类型和引用数据类型的区别

  • 储存位置不同
  • 复制结果不同
  • 访问方式不同

不用promise,等待所有请求回来后执行回调

  • 设置一个flag变量,然后在各自的ajax的成功回调内去维护这个变量数量(比如flag++),当满足条件时,我们来触发后续函数
  • 使⽤Generator
    // 使⽤Generator顺序执⾏三次异步操作
    function* r(num) {
      yield ajax1(num);
      yield ajax2();
      yield ajax3();
    }
    
    // ajax为异步操作,结合Promise使⽤可以轻松实现异步操作队列
    function ajax1(num) {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('第1个异步请求'); // 输出处理结果
          resolve()
        }, 1000);
      });
    }
    
    function ajax2() {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('第2个异步请求'); // 输出处理结果
          resolve(); // 操作成功
        }, 1000);
      });
    }
    
    function ajax3() {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('第3个异步请求'); // 输出处理结果
          resolve(); // 操作成功
        }, 1000);
      });
    }
    
    // 不使⽤递归函数调⽤
    let it = r();
    
    // 修改为可处理Promise的next
    function next(data) {
      let { value, done } = it.next(data); // 启动
      if (!done) {
        value.then(() => {
          next();
        });
      } else {
        console.log('执行完毕!');
      }
    }
    
    next();
    

new出来对象、构造函数、普通函数、object之间原型和原型链的区别和联系

new出来的对象原型指向构造函数的原型对象

构造函数的原型指向其构造函数的原型对象,最终指向Object的构造函数的原型对象,原型链的顶端都是null

Number创建出来的数值,instanceof判断出来是什么类型

instanceof这个运算符是用来测试一个对象的原型链上是否有该原型的构造函数,即instanceof左表达式要是一个对象,右侧表达式要是一个构造函数,并且左侧是右侧实例化出来的对象才会返回true

123 instanceof Number      
new Number(123) instanceof Number     
Number(123) instanceof Number

第一个 首先左侧为Number类型,并不是一个对象,更不是由Number实例化出来的(基本包装类型),所以为false

第二个 左侧使用Number构造实例化对象 右侧为Number构造 ,所以为true

第三个 左侧没有使用new所以并不是使用构造函数实例化 而是使用Number这个函数返回了一个数字, 所以为false

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

  • 箭头函数不能作为构造函数
  • 箭头函数不绑定arguments,取而代之用rest参数解决
  • this取决于父作用域的this
  • 箭头函数的this不通过call,apply,bind绑定
  • 箭头函数没有prototype

箭头函数为什么不能作为构造函数

new一个构造函数的时候通常会经过以下几步:

  • 创建空对象
  • __proto __指向构造函数的prototype
  • 确定this指向
  • 向对象中添加属性
  • 返回对象

首先箭头函数没有prototype,所以不能确定原型指向;

其次箭头函数的this是取决于父作用域的this,并且不能通过call,apply或者bind进行绑定的

字符串转换成json对象

  1. javascript函数eval() 语法:

    var obj = eval ("(" + txt + ")");  //必须把文本包围在括号中,这样才能避免语法错误
    

    eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。由于 JSON 语法是 JavaScript 语法的子集,JavaScript 函数eval()可用于将 JSON 文本转换为 JavaScript 对象。

    注意:当字符串中包含表达式时,eval() 函数也会编译并执行,转换会存在安全问题。

  2. 浏览器自带对象JSON.parse() 语法:

var obj = JSON.parse(text[, reviver])
//text:必需, 一个有效的 JSON 字符串。解析前要确保你的数据是标准的 JSON 格式,否则会解析出错。
//reviver: 可选,一个转换结果的函数, 将为对象的每个成员调用此函数。

JSON.parse()比eval()安全,而且速度更快。支持主流浏览器:Firefox 3.5,IE 8,Chrome,Opera 10,Safari 4。

注意:IE8兼容模式,IE 7,IE 6,会存在兼容性问题。

json和xml两者的区别

JSON 与 XML 的相同之处:

  • JSON 和 XML 数据都是 "自我描述" ,都易于理解。
  • JSON 和 XML 数据都是有层次的结构
  • JSON 和 XML 数据可以被大多数编程语言使用
  • JSON 和 XML 都用于接收 web 服务端的数据。

JSON 与 XML 的不同之处:

  • JSON 不需要结束标签
  • JSON 更加简短
  • JSON 读写速度更快
  • JSON 可以使用数组

最大的不同是:XML 需要使用 XML 解析器来解析,JSON 可以使用标准的 JavaScript 函数来解析。

Promise先resolve再reject,promise是什么状态;Promise先resolve后打印东西会成功吗

示例一:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject();
    },1000)
});
console.log(promise);//rejected

示例2:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('resolve前Promise的状态:',promise);//pending
        resolve();
        console.log('resolve后Promise的状态:',promise);//fulfilled
    })
});

示例3:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("Promise的状态:resolve前:",promise);//pending
        resolve();
        console.log("Promise的状态:resolve后:",promise); //fulfilled
        reject();
        console.log("Promise的状态:reject后:",promise);//fulfilled
    })
});

示例4:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("Promise的状态:reject前:",promise);//pending
        reject();
        console.log("Promise的状态:reject后:",promise);//rejected
        resolve();
        console.log("Promise的状态:resolve后:",promise);//rejected
    })
});
console.log('最终状态',promise)//pending

Promise怎么处理异常

promise.catch()可以捕获promise所有状态的异常。包括:

  1. 执行resolve()和reject()对应的promise.then(()=>{},()=>{}) 中的俩回调函数中的异常
  2. Promise.resolve(err)触发的
  3. Promise.reject(err)触发的
// 定义Promise
const initPromise = (status) => {
    return new Promise((resolve, reject) => {
        // status 成功 200,失败 其它
        if (status === 200) {
            resolve(); // 由"pending"变为"fulfilled"
        } else {
            reject('reject reason'); // 由"pending"变为"rejected"
        }
    });
};

// 实例化并调用promise
let testPromise = (status) => {
    const promise = initPromise(status);
    try {
        promise.then(
            () => {
                // resolve走这个回调
                console.log('resolve回调')
                throw new Error('error from then resolve');
            },
            (err) => {
                // rejected走这个回调
                console.log('reject回调',err)
                throw new Error('error from then reject');
            })
            .catch(e => {
                console.log('promise catch捕获:' + e);
            });
    } catch (e) {
        console.log('try catch捕获:' + e);
    }
}

testPromise(100)
//reject回调 reject reason
//promise catch捕获:Error: error from then reject

testPromise(200)
//resolve回调
//promise catch捕获:Error: error from then resolve
// 定义Promise
const initPromise = (status) => {
    return new Promise((resolve, reject) => {
        // status 成功 200,失败 其它
        setTimeout(() => {
            if (status === 200) {
                resolve(); // 由"pending"变为"fulfilled"
            } else {
                reject('reject reason'); // 由"pending"变为"rejected"
            }
        }, 0)
    });
};

// 实例化并调用promise
let testPromise = (status) => {
    const promise = initPromise(status);
    try {
        promise.then(
            () => {
                console.log('resolve回调')
                throw new Error('error from then resolve');

            },
            (err) => {
                // rejected走这个回调
                console.log('reject回调', err)
                throw new Error('error from then reject');
            })
            .catch(e => {
                console.log('promise catch捕获:' + e);
            });


    } catch (e) {
        console.log('try catch捕获:' + e);
    }
}

testPromise(200)
//resolve回调
//promise catch捕获:Error: error from then resolve

由上可以总结出:

  1. promise.catch可以处理resolve和reject回调函数抛出的异常;setTimeout中的 throw new Error不可以处理,但是可以处理setTimeout中的reject
  2. try...catch只能捕获try中抛出的异常

try...catch 无法捕获 setTimeout 和ajax请求中的异步任务中的错误。可以用window.onerror处理

// 定义Promise
const initPromise = (status) => {
    return new Promise((resolve, reject) => {
        // status 成功 200,失败 其它
        if (status === 200) {
            resolve(); // 由"pending"变为"fulfilled"
        } else {
            reject('reject reason'); // 由"pending"变为"rejected"
        }
    });
};

// 实例化并调用promise
let testPromise = (status) => {
    const promise = initPromise(status);
    try {
        promise.then(
            () => {
                // -------------------------
                try {
                    // resolve走这个回调
                    setTimeout(() => {
                        console.log('resolve回调')
                        throw new Error('error from then resolve');
                    }, 0)
                } catch (e) {
                    console.log('resolve里边的catch')
                }
                // -------------------------
            },
            (err) => {
                // rejected走这个回调
                console.log('reject回调', err)
                throw new Error('error from then reject');
            })
            .catch(e => {
                console.log('promise catch捕获:' + e);
            });


    } catch (e) {
        console.log('try catch捕获:' + e);
    }
}

testPromise(200)
//resolve回调
//Uncaught Error: error from then resolve

// 定义Promise
const initPromise = (status) => {
    return new Promise((resolve, reject) => {
        throw new Error('error!!!')
    });
};

// 实例化并调用promise
let testPromise = (status) => {
    const promise = initPromise(status);
    try {
        promise.then(
            () => {
            
            },
            (err) => {
                // rejected走这个回调
                console.log('reject回调', err)
                throw new Error('error from then reject');
            })
            .catch(e => {
                console.log('promise catch捕获:' + e);
            });

    } catch (e) {
        console.log('try catch捕获:' + e);
    }
}

testPromise(200)
//reject回调 Error: error!!!
//index1.js:32 promise catch捕获:Error: error from then reject

try...catch可不可以捕获promise异常

不能,try...catch只能捕获同步异常,对于异步异常不能捕获;

promise中的异常可以用promise.catch进行处理;

async/await

async function run() {
    try {
        await Promise.reject(new Error("Oops!"));
    } catch (error) {
        console.log(error.message)// "Oops!"
    }
}
run()

TypeScript

typeScripttypeinterface的区别

选择:能用interface就用interface,不能用的用type

相同点:

  • 都可以用来描述一个函数或者对象

    interface User {
      name: string
      age: number
    }
    
    interface SetUser {
      (name: string, age: number): void;
    }
    
    
    type User = {
      name: string
      age: number
    };
    
    type SetUser = (name: string, age: number)=> void;
    
    
  • 都允许拓展:但是语法不同

    //interface extends interface
    interface Name { 
      name: string; 
    }
    interface User extends Name { 
      age: number; 
    }
    
    // type extends type
    type Name = { 
      name: string; 
    }
    type User = Name & { age: number  };
    
    // interface extends type
    type Name = { 
      name: string; 
    }
    interface User extends Name { 
      age: number; 
    }
    
    //type extends interface
    interface Name { 
      name: string; 
    }
    type User = Name & { 
      age: number; 
    }
    
    

不同点:

  1. type可以而interface不行

    • type 可以声明基本类型别名,联合类型,元组等类型

      // 基本类型别名
      type Name = string
      
      // 联合类型
      interface Dog {
          wong();
      }
      interface Cat {
          miao();
      }
      
      type Pet = Dog | Cat
      
      // 具体定义数组每个位置的类型
      type PetList = [Dog, Pet]
      
      
      

  • type 语句中还可以使用 typeof 获取实例的 类型进行赋值

    // 当你想获取一个变量的类型时,使用 typeof
    let div = document.createElement('div');
    type B = typeof div
    
  1. interface 可以而 type 不行

    • interface 能够合并重复声明,而重复声明 type ,会报错

      interface User {
        name: string
        age: number
      }
      
      interface User {
        sex: string
      }
      
      /*
      User 接口为 {
        name: string
        age: number
        sex: string 
      }
      */
      
      

typeScript定义挂载在window上的变量类型

在index.d.ts文件中声明:

declare global{
	interface Window{
		val:string
	}
}

.d.ts文件中声明的变量或者模块,在其他文件中不需要使用import导入,可以直接使用。

.d.ts文件中我们常常可以看到declaredeclare左右就是告诉TS编译器你担保这些变量和模块存在,并声明了相应类型,编译的时候不需要提示错误!

declare 定义的类型只会用于编译时的检查,编译结果中会被删除。

我们在编写 TS 的时候会定义很多的类型,但是主流的库都是 JS编写的,并不支持类型系统。这个时候你不能用TS重写主流的库,这个时候我们只需要编写仅包含类型注释的 d.ts 文件,然后从您的 TS 代码中,可以在仍然使用纯 JS 库的同时,获得静态类型检查的 TS 优势。

转载自:https://juejin.cn/post/7196842170639925305
评论
请登录