js面试题总结

318 阅读13分钟

1.数据类型 ?

在 JS 中,存在着 7 种原始值,分别是:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol
  • bigint

引用数据类型: 对象Object(包含普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学函数-Math,函数对象-Function)

基本数据类型用存储,引用数据类型用存储,注意:闭包是 存储

问题1: 简单介绍一下symbol? 新的原始数据类型Symbol,表示独一无二的值 详情参考: https://es6.ruanyifeng.com/#docs/symbol

问题2: 简单介绍一下bigint?

BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。

这导致JS中的Number无法精确表示非常大的整数,它会将非常大的整数四舍五入,JS中的Number类型只能安全地表示-9007199254740991(-(2^53-1))和9007199254740991((2^53-1)),任何超出此范围的整数值都可能失去精度。

创建并使用BigInt

要创建BigInt,只需要在数字末尾追加n即可。

console.log( 9007199254740995n );    // → 9007199254740995n	
console.log( 9007199254740995 );     // → 9007199254740996
复制代码

另一种创建BigInt的方法是用BigInt()构造函数、

BigInt("9007199254740995");    // → 9007199254740995n
复制代码

详细链接:juejin.cn/post/684490…

问题3: 0.1+0.2 为什么不等于 0.3?

0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004。

小数如何计算? 可以先转换成整数,再转成小数,在保留2位

举个例子:

(drugNumber * (drugPrice * 100)) /100.toFixed(2)

2.如何判断数据类型?

1.typeof 判断数据类型

let arr=[];
let a=1;
let b="";
let c=true;
let d={};
let e=null;
let f=undefined;

console.log(arr, typeof arr); //[] "object"
console.log(a, typeof a);// 1 "number"
console.log(b, typeof b);//  string
console.log(c, typeof c);// true "boolean"
console.log(d, typeof d); // {} "object"
console.log(e, typeof e);// null "object"
console.log(f, typeof f);// undefined "undefined"

2.使用instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型, instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型

console.log(true instanceof Boolean);// false
console.log(1 instanceof Number);// false
console.log('abc' instanceof String);// false
console.log(undefined instanceof Object);// false
console.log([1,2,3]] instanceof Array);// true
console.log(null instanceof Object);// false
console.log({name:"hao"}} instanceof Object);// true
console.log(function(){} instanceof Function);// true

var bool2 = new Boolean()
console.log(bool2 instanceof Boolean);// true

var num2 = new Number()
console.log(num2 instanceof Number);// true

var str2 = new String()
console.log(str2 instanceof String);//  true

function Person(){}
var per = new Person()
console.log(per instanceof Person);// true

function Student(){}
Student.prototype = new Person()
var haoxl = new Student()
console.log(haoxl instanceof Student);// true
console.log(haoxl instanceof Person);// true

从结果中看出instanceof不能区别undefined和null,而且对于基本类型如果不是用new声明的则也测试不出来,对于是使用new声明的类型,它还可以检测出多层继承关系。

3、使用constructor

constructor是原型prototype的一个属性,当函数被定义时候,js引擎会为函数添加原型prototype,并且这个prototype中constructor属性指向函数引用, 因此重写prototype会丢失原来的constructor。 undefined和null没有contructor属性

console.log(true.constructor === Boolean);// true
console.log(1.constructor === Number);// true
console.log("abc".constructor === String);// true
console.log([1,2,3]].constructor === Array);// true
console.log({name:'hao'}.constructor === Object);// true
console.log(function(){}.constructor === Function);// true

console.log(haoxl.constructor === Student);// false
console.log(haoxl.constructor === Person);// true

constructor不能判断undefined和null,并且使用它是不安全的,因为contructor的指向是可以改变的

4.toString 是最完美的

toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。 对于 Object 对象,直接调用 toString()  就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用

那么如何判断一个数组呢?

方法1:Array.isArray()

let a = [1,2,3];
Array.isArray(a); // true

方法2:Object.prototype.toString.call([])

Object.prototype.toString.call([]) ; // [object Array]

方法3:constructor

console.log([1,2,3]].constructor === Array);// true

方法4:使用instanceof

console.log([1,2,3]] instanceof Array);// true

3.介绍一下作用域 及作用域链?

  • es5中只分 1.全局作用域、2.函数作用域
  • es6 新增 块级作用域 当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链
  1. 全局作用域:最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的
  2. 函数作用域:(局部作用域)一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的

4.let var const 不写 ?

注意:!!!不写的话,你实际上声明了一个全局变量。

1.变量提升 var声明变量存在变量提升 ,let和const不存在变量提升

console.log(a); // undefined  ===>  a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:b is not defined  ===> 找不到b这个变量
let b = 10;
console.log(c); // 报错:c is not defined  ===> 找不到c这个变量
const c = 10;

2.let、const都是块级局部变量,块之外访问会报错

{
    let a = 1
    var b = 1;
}
console.log(a) // undefined
console.log(b) // 1

注意:!!!!

  1. const 声明的时候必须赋值
  2. 同一作用域下let 和const不能声明同名变量,而var可以。

3.暂时性死区: 在代码块内,使用let和const命令声明变量之前,该变量都是不可用的,语法上被称为暂时性死区。

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

4.let 和const不同点

let:

  • 声明的变量可以改变,值和类型都可以改变,没有限制。

const:

  1. const 声明之后必须马上赋值,否则会报错
  2. const 简单类型一旦声明就不能再更改,复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改

5.闭包

68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31312f31362f313637316432666132666563396434353f773d39303526683d34383626663d706e6726733d3430363233.png

详情参考: github.com/ljianshu/Bl…

6.原型链?

图片1.png

7.继承的几种实现方式?

  1. call
  function Parent1(){
    this.name = 'parent1';
  }
  function Child1(){
    Parent1.call(this);
    this.type = 'child1'
  }
  console.log(new Child1);
  1. 原型链
  function Parent2() {
    this.name = 'parent2';
    this.play = [1, 2, 3]
  }
  function Child2() {
    this.type = 'child2';
  }
  Child2.prototype = new Parent2();

  console.log(new Child2());
 
  1. 组合

  2. 优化组合

  3. 寄生组合继承

  4. extends 详情参考: juejin.cn/post/684490…

8.数组都有哪几种遍历方式?

  1. for
  2. while
  3. map
  4. forEach
  5. for of
  6. for in
  7. filter
  8. some
  9. every
  10. reduce
  11. includes
  12. keys()、values()、entries()
  13. flat
  14. flatMap()
  15. find()
  16. findIndex()

juejin.cn/post/697070…

9.对象的遍历方式呢?

  1. for...in

for...in循环遍历对象自身的和继承的可枚举属性(不包含Symbol属性)

  1. Object.keys(obj)

Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含symbol属性)

  1. Object.values

  2. Object.entries

juejin.cn/post/698667…

10.js异步 promise

  1. 介绍一下promise?
1.创造了一个Promise实例
const promise = new Promise(function(resolve, reject) {
  // ... some code
if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
2.Promise.prototype.then()

then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function (comments) {
  console.log("resolved: ", comments);
}, function (err){
  console.log("rejected: ", err);
});
3.Promise.prototype.catch()
// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch

4.Promise.prototype.finally()

方法用于指定不管 Promise 对象最后状态如何,都会执行的操作tch()方法,而不使用then()方法的第二个参数

5.Promise.all()
const p = Promise.all([p1, p2, p3]);

只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数

6.Promise.race()
const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

7.Promise.allSettled()

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。

Promise.all()无法确定所有请求都结束。想要达到这个目的,写起来很麻烦,有了Promise.allSettled(),这就很容易

8.Promise.any()

ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

Promise.any()跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束

9.Promise.resolve()
10.Promise.reject()
  1. 实现一个promise all?
let p1=new Promise(resolve=>resolve('p1'));
let p2=new Promise(resolve=>resolve('p2'));	
let p3=Promise.reject('p3 promise');

      function promiseAll(promises){
       return new Promise ((resolve,reject)=>{
        let resultCount = 0;
        let results = new Array(promises.length);
        for(let i=0;i<promises.length;i++){
         promises[i].then(item=>{
          resultCount++;
          results[i]=item;
          if(resultCount==promises.length){
          return resolve(results);
          }
         },error=>{
          return reject(error)
         })
        } 
       })
      }

	promiseAll([p1,p2,p3]).then(result=>{
	   console.log('111',result)
	}).catch(error=>{
	   console.log(error)
	})
  1. 实现一个 promise?
const Resolve = "resolved";
const Reject = "rejected";
const Pending = "pending";
const handlePromise=(result,newPromise,resolve,reject)=>{
  if(result===newPromise){ // 如果返回值 是一个newPromise 则没有意义
    throw new Error("can not return oneself");
  }
  if((typeof result === "object" && typeof result !==null) 
  || typeof result ==="function"){ // 判断返回的是不是一个promise
    let lock =false;
    try{
      const then = result.then;
    if(typeof then === "function"){
      then.call(result,(r)=>{ // 如果返回的是 newPromise.then 则执行
        if(lock){ // 如果promise是第3方的 需要执行 resolve 和reject 
          return;
        }
        handlePromise(r,newPromise,resolve,reject); 
        // 当promise 返回 {name:'123',then:123}
        lock=true;
      },(e)=>{
        if(lock){
          return;
        }
        reject(e);
        lock=true;
      })
    }else{
      resolve(result);
    }
    }catch(error){
      reject(error)
    }
    

  }else{
    resolve(result);
  }

};
class JJPromise{
  status = Pending;
  result = undefined;
  reason = undefined;
  onResolvedArr=[];
  onRejectedArr=[];
  constructor(excution){
    const resolve = (result) => {
      if(this.status === Pending){
        this.result = result;
        this.status = Resolve;
        this.onResolvedArr.map((fn)=>fn());
      }

    };
    const reject = (reason) => {
      if(this.status === Pending){
        this.reason = reason;
        this.status = Reject;
        this.onRejectedArr.map((fn)=>fn());
      }
    };
    try{
      excution(resolve,reject)
    }catch(error){
      reject(error)
    } 
  }
  then(onResolved,onRejected){
    onResolved = typeof onResolved ==="function"? onResolved:(data)=>data;
    onRejected = typeof onRejected ==="function"? onRejected:(err)=>{
    throw new Error(err)
    };
    const newPromise = new JJPromise((resolve,reject)=>{ //实现链式调用
      if(this.status === Resolve){
      setTimeout(()=>{ // 添加异步队列 解决 执行顺序问题
        try{
          const result = onResolved(this.result);
          handlePromise(result,newPromise,resolve,reject)
        }catch(error){
          reject(error)
        }
      },0)   
    }
    if(this.status === Reject){
      setTimeout(()=>{
        try{
          const result = onRejected(this.reason);
          handlePromise(result,newPromise,resolve,reject)
        }catch(error){
          reject(error)
        }
      },0)  
    }
    if(this.status === Pending){ 
    // 发布订阅模式 解决  promise 中 setTimeOut(()=>{resolve("1111")},1000); 执行问题
      this.onResolvedArr.push(()=>{
        try{
          const result = onResolved(this.result);
          handlePromise(result,newPromise,resolve,reject) //链式调用的第二个promise 执行,否则不执行
        }catch(error){
          reject(error)
        }
      });
      this.onRejectedArr.push(()=>{
        try{
          const result = onRejected(this.reason);
          handlePromise(result,newPromise,resolve,reject)
        }catch(error){
          reject(error)
        }
      });
    }
    })

    return newPromise;

}
  catch(onRejected){
    return this.then(undefined,onRejected)

  }
}

module.exports = JJPromise;



const test = new JJPromise((resolve,reject)=>{
  resolve("我们是最棒的");
})
test.then((res)=>{
  console.log(res);
}).then((res)=>
  console.log("1111")
);

console.log(123);


  1. 写出指定代码的输出内容?
const first = () =>{
    new Promise((resolve,reject)=>{
           console.log(3);
       let p = new Promise((resolve,reject)=>{
           console.log(7);
           setTimeout(()=>{
           console.log(5);
           resolve(6);
           },0);
           resolve(1);
       });
       resolve(2);
       p.then(arg =>{
       console.log(arg);
       });
    })
}
first().then(arg =>{
    console.log(arg);
})
 console.log(4);

1.同步的代码(最高) 3 7 4 2.微任务的异步代码(次高,then) 1 2 3.宏任务的异步代码 (最低,setTimeout) 5

注意:

  • 为什么没有6?一个Promise 里面只能调用一个resolve,因此先调用了1 就不会再调用6。
  • 多个Promise ,如何判断 resolve 对应于哪个Promise? 就近原则

Promise 是异步编程的一种解决方案,用来解决回掉地狱的问题。

中文版源码介绍参考: juejin.cn/post/684490…

es6阮一峰老师: https://es6.ruanyifeng.com/#docs/promise

11.async await?

es6阮一峰老师: https://es6.ruanyifeng.com/#docs/async

12.Generator?

es6阮一峰老师: es6.ruanyifeng.com/#docs/gener…

13.事件循环?

github.com/lgwebdream/…

14.防抖截流?

  1. 防抖 定义:对于短时间连续触发的事件,(如滚动事件)防抖的含义就是在某个时间期限内(如1000),事件处理函数只执行一次

<!DOCTYPE html>
<html>
<body>
    <p>如果小时小于18:00,显示“美好的一天!”:</p>
    <p id="demo">晚安</p>
    <div>!!!!!</div>
    <div>!!!!!</div>
    <div>!!!!!</div>
    <div>!!!!!</div>
    <div>此处省略多个div!!!!!</div>

    
    <script>
        // 此方法就实现了,停止滚动1s以后,才会打印出滚动条位置
        function debounce(fn, delay) {
            let timer = null; // 借助闭包
            return function () {
                if (timer) {
                    clearInterval(timer);
                }
                timer = setTimeout(fn, delay);
            }
  
        }

        // 监听浏览器滚动事件,返回当前滚动条与顶部距离
        function showTop() {
            var scrollTop = document.body.scrollTop
            || document.documentElement.scrollTop;
            console.log('滚动位置', scrollTop); 
            // 那么问题来了,浏览器执行这个函数的频率太高了,浏览器性能有限,于是引入防抖
        }
        window.onscroll = debounce(showTop, 1000);
    </script>
</body>
</html>


  1. 节流 如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定时间期限内不在工作,直到过段时间才重新生效。 比如 秒杀
<!DOCTYPE html>
<html>

<body>

    <p>如果小时小于18:00,显示“美好的一天!”:</p>

    <p id="demo">晚安</p>
    <div>!!!!!</div>
    <div>!!!!!</div>
    <div>!!!!!</div>
    <div>!!!!!</div>
    <div>此处省略多个div!!!!!</div>
    <script>
        // 监听浏览器滚动事件,返回当前滚动条与顶部距离
        function showTop() {
            var scrollTop = document.body.scrollTop 
            || document.documentElement.scrollTop;
            console.log('滚动位置', scrollTop); 
        }
        function throttle(fn, delay) {
            let valid = true;
            return function () {
                if (!valid) {
                    return false;
                }
                valid = false;
                setTimeout(() => {
                    fn();
                    valid = true;
                }, delay);
            }

        }
        window.onscroll = throttle(showTop, 1000);
    </script>
</body>

</html>


15.Proxy?

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程

es6.ruanyifeng.com/#docs/proxy

var proxy = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。 es6.ruanyifeng.com/#docs/proxy

16.map和object区别?

1.键值对:

Object本质上是哈希结构的键值对的集合,它只能用字符串、数字或者Symbol等简单数据类型当作键。Map的键可以是任意的数据类型。

2.可迭代:

Map实现了迭代器,可用for...of遍历,而Object不行。

3.长度:

Map可以直接拿到长度,而Object不行。

4.有序性:

填入Map的元素,会保持原有的顺序,而Object无法做到。

5.可展开:

Map可以使用省略号语法展开,而Object不行。

18.深浅拷贝?

浅拷贝:主要拷贝的是对象的引用值,当改变对象的值,另一个对象的值也会发生变化

let obj={a:1,b:{i:2,j:3}};
let obj1={};
// es5实现方式
for(let key in obj){
	if(!obj.hasOwnProperty(key))break;
    obj1[key]=obj[key];
}
// es6实现方式
obj1={
	...obj
};

深拷贝:深拷贝主要是将另一个对象的属性值拷贝过来之后,另一个对象的属性值并不受到影响,因为此时它自己在堆中开辟了自己的内存区域,不受外界干扰

let obj1=JSON.parse(JSON.stringify(obj)); // 弊端?

1.函数 2.日期格式 3.正则 都在json.stringify 出现问题

采用递归?

function deepClone(obj){
	// =>过滤特殊情况
     if(obj===nullreturn null;
     iftypeof obj!=="object"return obj;
     if(obj instanceof RegExp){
     	return new RegExp(obj);
     }
      if(obj instanceof Date){
     	return new Date(obj);
     }

    let newObj = Array.isArray(target) ? []: {};
    forlet key in obj){
	if(obj.hasOwnProperty(key)){
       newObj[key]=deepClone(obj[key]);
    }
  
}
return newObj;

}

再深入一点? 循环引用问题的产生原因可能是对象之间相互引用,也可能是对象引用了其自身,而造成死循环的原因则是我们在进行深拷贝时并没有将这种引用情况考虑进去,因此解决问题的关键也就是可以将这些引用存储起来并在发现引用时返回被引用过的对象,从而结束递归的调用。

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null; 
const deepClone = (target, map = new WeakMap()) => {

  if(map.get(target))
      return target; 
  
  if (isObject(target)) {
      map.set(target, true); 
      
      const cloneTarget = Array.isArray(target) ? []: {};
      
      for (let prop in target) { 
            if (target.hasOwnProperty(prop)) {
                cloneTarget[prop] = deepClone(target[prop],map); 
            }
          }
          return cloneTarget;
  } else { 
      return target;
  } 
}

let a = [1, 2];
let b = [4, 5, 6, a];
a.push(b);
let c = deepClone(a);

console.log(c)



小提示 :

  1. WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
  2. WeakMap的键名所指向的对象,不计入垃圾回收机制。