总结Ⅲ-基础篇之JS

335 阅读16分钟

JS

1. 知道哪些ES6语法,怎么用

举例回答:

  1. let : 声明变量。只在它的块级作用域内有效;没有变量提升,必须先声明后使用;不能重复声明。

  2. const : 声明一个只读的常量。一旦声明,常量的值就不能改变。只在声明所在的块级作用域内有效;一旦声明变量,就必须立即初始化,不能留到以后赋值;不提升;不可重复声明。

  3. 解构赋值:数组的解构:let [a,b,c]=[1,2,3] 从数组中取出值,根据对应的位置赋给变量。 对象的解构:let a=obj.a 等价于let {a}=obj

  4. ...扩展运算符:接一个数组,把数组变成一个参数序列

  5. class:类关键字

  6. 模块功能export import:export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

    分为两种:具名导出:export {fn1,fn2} 导入:import {fn1,fn2} from 'xxx.js' 导入的必须和导出的同名 默认导出:export default fn1 导入:import f from 'xxx.js' 可以任意名称

  7. 箭头函数:写起来简短,没有this

  8. 默认参数:函数默认参数允许在没有值或undefined被传入时使用默认形参。

2. Promise,Promise.all,Promise.race 怎么用

Promise是异步编程的一种解决方案,比传统的回调函数要更方便,合理,避免了回调地狱。

Promise对象只有三种状态:进行中,已成功,已失败。一个Promise对象代表了一个异步操作。这个状态只能改变一次。进行中->成功,进行中->失败。

用法:new Promise创建一个promise实例,Promsie构造函数接受一个函数作为参数。这个函数的两个参数分别是resolve和reject,它们也是函数。resolve函数会在异步操作成功后被调用,把异步操作的结果作为参数传递出去;reject函数会在异步操作失败后被调用,把错误的信息作为参数传递出去。

创建了promise实例后,通过then方法给状态变化时添加回调函数。then接受两个回调函数作为参数,第一个是状态变为resolve时被调用,第二个状态变为reject被调用。

Promise.all: 接收多个promise实例作为参数,包装成一个新的promise实例const p = Promise.all([p1, p2, p3]);p的状态由p1、p2、p3决定,分成两种情况。

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

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

Promise.race: 同样是将多个 Promise 实例,包装成一个新的 Promise 实例.const p = Promise.race([p1, p2, p3]);只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

3. 手写函数防抖和函数节流

在时间轴上控制函数的执行次数。

  1. 函数防抖:在事件被触发n秒后,再执行它的回调函数。如果在这n秒的期间又被触发了,就再次重新计时。总的来说,适合多次事件一次响应的情况

    任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。

    就像进电梯,第一个人进电梯(触发事件),电梯不会马上运行,而是等10秒钟,如果这10秒内又有人上来了(又被触发了),就重新计时10秒钟。直到10秒钟内没人再上来了,电梯就运行了。

    function debounce(fn,delay){
        let timer=null
        return function(){
            let context=this
            if(timer){
                window.clearTimeout(timer)
            }
            timer=setTimeout(()=>{
                fn.apply(context,arguments)
                timer=null
            },delay)
        }
    }
    const debounced=debounce(()=>{console.log('hi')})
    debounced()
    debounced()
    

    关于为什么fn要用apply调用:因为可能回调函数fn需要参数,或者需要指定this。这些信息只能通过debounced 函数传进去,也就是debounce里return的那个匿名函数。所以需要保持fn被调用的时候,它里边的this和参数与debounced传进去的一致,才能将这些信息给到fn。如果不用apply,而是直接fn() 去调用,那么fn里的this就是window了。所以需要用apply,去指定fn里边的this和参数。

    如果setTimeout里用的是箭头函数,在return的函数里,不将this赋给context也行,也就是这句let context=this 可以删掉。直接fn.apply(this,arguments)就可以。因为setTimeout里用的是箭头函数,所以里边的this和外边的一样,也就是和传给debounced的this一样。

    但是如果setTimeout里用的是匿名函数,就需要先保存this了。原因:关于setTimeout的this指向

  2. 函数节流:理解为冷却时间,一次技能发出后,必须间隔一段时间才能再次发出。怎么实现?利用闭包,为true就可以执行,执行后置为false,然后规定,几秒钟后再把该标记置为true

    function throttle(fn,delay){
        let canuse=true
        return function(){
            if(canuse){
                fn.apply(this,arguments)
                canuse=false
                setTimeout(()=>{
                    canuse=true
                },delay)
            }
        }
    }
    const throttled=throttle(()=>{console.log('hi')})
    throttled()
    throttled()
    

    一段时间执行一次之后,就不执行第二次

4. 手写AJAX

  1. 背代码,完整版

     const request=new XMLHttpRequest()
    request.open('GET','/xxx')
    request.onreadystatechange=()=>{
        if(request.readyState===4){
            console.log('下载完成')
            if(request.status>=200 && request.status<300){
                console.log('请求成功')
            }else{
                console.log('请求失败')
            }
        }
    }
    request.send()
    
  2. 背代码,简化版

     var request = new XMLHttpRequest()
     request.open('GET', '/a/b/c?name=ff', true)
     request.onload = ()=> console.log(request.responseText)
     request.send()
    

5. 这段代码里的this是什么

  1. 背代码

    1. fn()
      this => window/global
    2. obj.fn()
      this => obj
    3. fn.call(xx)
      this => xx
    4. fn.apply(xx)
      this => xx
    5. fn.bind(xx)
      this => xx
    6. new Fn()
      this => 新的对象
    7. fn = ()=> {}
      this => 这个函数外面的 this
  2. 看调用
    《this 的值到底是什么?一次说清楚》

6.1 什么是闭包

  • 要点:阐述什么是闭包,闭包的作用:

一个函数内部使用了外部的变量,那么这个函数和变量就构成了一个闭包。

闭包的作用:隐藏一个变量,间接访问一个变量。

function foo(){
  var local = 1
  function bar(){
    local++
    return local
  }
  return bar
}

var func = foo()
func()

比如这样,一个函数嵌套一个函数,return这个函数。调用外边的函数就会得到一个接口,用这个接口访问局部变量。local和bar就构成了一个闭包。

通过使用闭包,我们可以实现在一个函数外边访问到它的内部变量。通常这是一个父函数,我们想在父函数外部访问它的局部变量,可以在它的内部声明一个子函数,这个子函数是可以使用父函数的内部变量的,所以我们return 这个子函数。在外边通过子函数这个接口访问父函数的内部变量。

闭包还会让这些局部变量的值始终保存在内存里。比如foo调用完以后,local并不会被回收。因为子函数bar被引用给了一个全局变量,所以bar要一直存在,而bar的存在又依赖它的父函数,所以foo函数也会一直保存在内存里。

闭包的缺点:闭包会使函数里的变量一直在内存里,导致内存消耗很大。

6.2 什么是立即执行函数,有什么作用

js立即执行函数可以让你的函数在创建后立即执行,js立即执行函数模式是一种语法,可以让你的函数在定义后立即被执行,这种模式本质上就是函数表达式(命名的或者匿名的),在创建后立即执行。

但是这样写浏览器会报错,为了通过检查,我们一般有以下几种写法:

(function(){alert('我是匿名函数')} ()) // 用括号把整个表达式包起来
(function(){alert('我是匿名函数')}) () //用括号把函数包起来
!function(){alert('我是匿名函数')}() // 求反,我们不在意值是多少,只想通过语法检查。

作用:创建一个独立的作用域,使外边访问不到这个作用域里的内部变量,避免变量污染。

7. 什么是跨域,CORS,JSONP

浏览器出于安全方面的考虑,有一个叫做同源策略的规定,它规定,不同源的页面之间不允许互相访问数据。但是现实情况我们需要访问不同源的资源,跨域就是我们通过一些手段实现访问不同源的资源。

CORS和JSONP是实现跨域的两种方法。

CORS是跨域资源共享,比如某一个源想把自己的数据公开给谁,就在后台服务器添加一个响应头:

response.setHeader("Access-Control-Allow-Origin", "http://anqi.com:9999");后面可以指定允许谁访问。

我们在跨域的时候,由于当前浏览器不支持CORS,所以就需要另外一种方法来跨域。我们通过创建一个script标签,请求另外一个网站的一个JS文件,这个JS文件里通过执行一个回调,来获取我们想得到的数据。这个回调函数的名字是我们随机生成的,是一个随机数,我们常常以callback这个参数把函数名传到后台,后台得到这个名字再传给我们,然后执行这个函数。

JSONP的优点,兼容 IE,可以跨域。 缺点:由于JSONP是用script标签引用JS的,所以没有AJAX那么精确,它读不到状态码,也不知道响应头,只知道执行成功还是失败。并且,由于是script标签,只能发GET请求,不支持POST。

8. async和await怎么用,如何捕获异常?

async是什么?与promise有关;让异步函数更像是同步函数。

为什么要用await?和promise.then.then相比,用await可以让异步代码看起来更像是标准的同步代码。从上到下执行,但是promise代码的执行顺序不能直接看懂。

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: "resolved"
}

asyncCall();

使用async function 声明一个异步函数,这个异步函数返回一个Promise对象。await关键字只能用在async函数里,await后边接一个返回Promise的函数,并且调用这个函数。await会暂停当前的异步函数,等待Promise执行完毕。await 关键字只在异步函数内有效

用try catch捕获异常,把await放在try里

参考文章:async/await 和 promise 的执行顺序

理解 JavaScript 的 async/await

10分钟理解JS引擎的执行机制

9. 如何实现深拷贝?

  • 背代码,要点:

    1. 递归
    2. 判断类型
    3. 检查循环引用(也叫环)
    4. 需要忽略原型

前置知识:浅拷贝如果是复杂数据类型,只会拷贝地址,还是共用同一块内存。所以改变原对象也会影响拷贝的对象。深拷贝是从内存完整的拷贝一份,没有共用内存地址,两个对象互不影响。

实现深拷贝最简陋的做法就是JSON.parse(JSON.stringify(source)) 但是无法涵盖数组,循环引用的情况。

基本的做法:先创建一个空对象作为结果。对于要拷贝的参数,首先判断它的类型,如果是对象,就要递归调用,把深层次的属性依次拷贝。如果是基本数据类型,就直接返回。

但是如果是数组,就不能拷贝成对象的形式。需要在初始化时判断如果要拷贝的是一个数组类型,就初始化一个空数组,其他和对象一样。

循环引用:如果这个对象的一个属性对应的value是它自己,obj.a=obj 递归就会进入死循环。解决办法:需要一块存储空间保存新对象和旧对象的关系。在拷贝对象的每一个属性的时候,先去这个空间里找,这个对象有没有被拷贝过,如果已经被拷贝过了(引用自己的情况),那就结束本地递归,直接返回上次被拷贝过了的结果值;如果这个对象还没被拷贝过,是一个新的属性,就正常往下走,还要把当前这个对象和它的拷贝对象存进空间里。这个存储空间用map对象,因为map也是以key-value存储,而且key可以是任意类型。

function deepClone(source,map=new Map()){
    if(typeof source==='object'){
        let result=Array.isArray(source) ? []:{}
        if(map.get(source)){
            return map.get(source)
        }
        map.set(source,result)
        for(let key in source){
            result[key]=deepClone(source[key],map)
        }
        return result
    }else{
        return source
    }
}

参考文章:如何写出一个惊艳面试官的深拷贝?

10. js判断数据类型的三种方法

  1. typeof

    返回一个字符串。问题:无法正确返回复杂数据类型的类型。都是'object'


  1. instanceof

    a instanceof A 判断a是不是A的实例,返回true/false

    它可以判断出数组,函数,日期等引用类型

    [] instanceof Array; // true
    {} instanceof Object;// true
    newDate() instanceof Date;// true
    
    function Person(){};
    new Person() instanceof Person;  //true
    
    

  1. Object.prototype.toString.call

    用对象原型上的toString方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

    对于 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 的引用
    

11.1 用class实现继承

class Animal{
    constructor(type){
        this.type=type
    }
    move(){
        console.log('跑起来')
    }
}
class Dog extends Animal{
    constructor(type,name){
        super(type)//继承里面必须super
        this.name=name
    }
    eat(){}
}

11.2 不用class实现继承

function Animal(type){
    this.type=type
}
Animal.prototype.move=function(){}

function Dog(type,name){
    Animal.call(this,type)//调用父类函数
    this.name=name//自己的属性
}

// 实现原型的继承,即Dog.prototype.__proto__=Animal.prototype
let f=function(){}
f.prototype=Animal.prototype
Dog.prototype=new f()

//constructor变成Dog
Dog.prototype.constructor=Dog

//自己新的属性可以自己写
Dog.prototype.eat=function(){}

const dog=new Dog('dog','hi')

12. 用正则实现 trim()

去掉字符串开头和结尾的空白字符。

// /正则表达式/ 包含在斜杠之间就是正则表达式
// ^ 匹配输入字符串开始的位置
// \s 匹配空字符串,包括空格、制表符、换页符等
// * 或 + 匹配零次或多次
// | 或者
// $ 匹配输入字符串结束的位置
// g 全局搜索
function trim(string){
    return string.replace(/^\s+|\s+$/g,'')
}

如果不加g,只会去掉开头的空格,结尾的没去掉。

13. 如何解决异步回调地狱

promise、generator、async/await

14. 图片的懒加载和预加载

预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。

懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。

两种技术的本质:

两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。

懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

15. js的new操作符做了哪些事情

new 操作符新建了一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象。

16. 函数柯里化

什么是函数柯里化?把接受多个参数的函数,转换成一个接受单一参数的函数

就是函数里return 另一个函数

function sum(x){
  return function(y){
    return x+y
  }
}

sum(1)(2)   //3

普通方法:

function sum(x,y){
    return x+y
}

也就是说,原来实现两个数相加,要在一个括号里传两个参数。

利用函数柯里化,每次调用只传一个参数,返回一个匿名函数,再传一个参数调用,就会得到最后的结果。

三个数相加也同理:

function sum(x){
  return function(y){
    return function(z){
      return x+y+z
    }
  }
}
console.log(sum(1)(2)(3))  //6

17. null和undefined区别

阅读:undefined与null的区别-阮一峰

null表示"没有对象",即该处不应该有值。

典型用法是:

(1) 作为函数的参数,表示该函数的参数不是对象。

(2) 作为对象原型链的终点。


Object.getPrototypeOf(Object.prototype)
// null

undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。

典型用法是:

(1)变量被声明了,但没有赋值时,就等于undefined。

(2)调用函数时,应该提供的参数没有提供,该参数等于undefined。

(3)对象没有赋值的属性,该属性的值为undefined。

(4)函数没有返回值时,默认返回undefined。


var i;
i // undefined

function f(x){console.log(x)}
f() // undefined

var  o = new Object();
o.p // undefined

var x = f();
x // undefined

18. require和import的区别

require属于CommonJS模块规范,而import属于ES6模块规范。

区别主要有两点:

  1. require加载,只能在运行时加载,运行时在能确定模块的依赖关系,输入输出等。因为它加载的是一个对象,这个对象只有在脚本执行时才能得到。 而import可以在编译时就完成加载,因为它是静态导入,不是对象
  2. require加载得到的值是只是这个值的拷贝。加载完以后,原来模块里这个值的变化,反应不到外边。就算原来模块里的值变了,加载得到的也还是一开始的那个值。 而import导入的值是引用。导入的这个变量,会随着原来模块里的变量的变化一起变化。如果原来模块里的变了,那在外边读到的值也是新的值。

19. 什么是按需加载

当用户触发了动作时才加载对应的功能。触发的动作,是要看具体的业务场景而言,包括但不限于以下几个情况:鼠标点击、输入文字、拉动滚动条,鼠标移动、窗口大小更改等。加载的文件,可以是JS、图片、CSS、HTML等。