前端面试JS

目录

一、变量类型

1、值类型和引用类型

2、判断数据类型typeof、instanceof、Object.prototype.toString.call()、constructor

1)typeof

2)instanceof

3)Object.prototype.toString.call()

4)constructor

3、深拷贝与浅拷贝

4、“==”和“===”的区别

5、truely变量和falsely变量

6、let、const和var的概念与区别、变量提升与暂时性死区

7、变量的解构赋值

8、Symbol概念及其作用、应用场景

二、原型和原型链

1、原型和原型链、prototype与__proto__的关系与区别

2、class基本语法及继承

3、new的原理

4、new和Object.create的区别

三、作用域和闭包

1、作用域和作用域链、执行上下文

2、bind、call、apply的区别

3、如何正确判断this?

4、闭包及其作用

5、JS的垃圾回收机制

四、异步

1、概念

2、EventLoop事件循环/事件轮询、宏任务与微任务

3、Promise(Promise A+规范、手撕Promise.all、Promise.race、Promise相关API和方法)

4、async/await

5、Generator及其异步方面的应用

6、几种异步方式的比较(回调、Promise、Generator、async)

7、setTimeout用作倒计时为何会产生误差?

8、Iterator和for...of(Iterator遍历器的实现)

9、循环语法比较及使用场景(for、forEach、for...in、for...of)

10、defer和async的区别

五、JS Web API

1、DOM(Document Object Model)

2、BOM(Browser Object Model)

3、事件绑定、事件冒泡、事件代理(事件委托)

4、ajax和跨域

1)Ajax的请求过程

2)跨域:同源策略及跨域实现方式和原理

5、存储Cookie、localStorage、sessionStorage

6、其他

1)JS中的String、Math和Array方法

2)类数组与数组的区别与转换

3)数组扁平化

4)数组去重

5) JavaScript中的arguments

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

7)函数柯里化及其通用封装

8)ES6新增特性

9)Set和Map数据结构


一、变量类型

1、值类型和引用类型

值类型(基本数据类型):Number、String、Boolean、Undefined、Null、Symbol、BigInt。直接存储在栈(stack)中,占用空间小、大小稳定。

引用类型:对象Object、数组Array、函数Function。存储在堆(heap)中,占用空间大,大小不固定。同时会在栈中存储一个该对象的堆内存地址(指针)。

Number类型:0.1+0.2!==0.3原因?Number类型取值范围?

Undefined和Null类型区别?

Null(空值)表示一个为空的对象;

Undefined(未定义)声明一个变量没有赋值

JavaScript中值类型和引用类型的区别:

1、值类型的值是不可变的;引用类型的值是可变的

2、值类型的变量是存放在栈;引用类型在栈中存放的是变量标识符以及变量所对应值的引用地址,而变量所对应的值被存放在堆中

3、值类型无法添加属性和方法;引用类型可以添加属性和方法

4、值类型拷贝变量的内容;引用类型拷贝引用地址

参考:JavaScript 原始值和引用值的各种区别

2、判断数据类型typeof、instanceof、Object.prototype.toString.call()、constructor

1)typeof

typeof 对于值类型来说,除了 null 都可以显示正确的类型;对于引用数据类型,除了函数之外,都会显示"object"

描述结果
Undefined"undefined"
Null"object"
Boolean"boolean"
Number"number"
String"string"
Symbol"symbol"
BigInt"bigint"
Function"function"
Array"object"
Object"object"

参考:MDN

2)instanceof

instanceof用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性,其意思就是判断对象是否是某一数据类型(如Array)的实例。

js实现instanceof:

function myInstanceof(left,right){
  var protoType=right.prototype
  var proto=left.__proto__
  while(true){
    if(proto===null){
      return false
    }
    if(proto===protoType){
      return true
    }
    proto=proto.__proto__
  }
}

3)Object.prototype.toString.call()

对于 Object.prototype.toString() 方法,会返回一个形如 “[object XXX]” 的字符串。
若参数不为 null 或 undefined,则将参数转为对象,再作判断。对于原始类型,转为对象的方法即装箱。

4)constructor

constructor可以检测原始值类型,但是只会基于原型链上的直属类检测。

参考:typeof,instanceof,constructor,Object.prototype.toString.call()

3、深拷贝与浅拷贝

深拷贝和浅拷贝只针对 Object, Array 这样的引用类型。浅拷贝只复制一层对象的属性,而深拷贝则递归复制了所有层级。

深拷贝实现方式:

1、递归

function deepClone(obj={}){
    if(obj==null || typeof obj!=='object'){
        return obj
    }
    
    let result
    if(obj instanceof Array){
        result=[]
    }else{
        result={}
    }

    for(let key in obj){
        //保证key不是原型的属性
        if(obj.hasOwnProperty(key)){
            //递归调用
            result[key]=deepClone(obj[key]);
        }
    }
    
    return result
}

2、JSON.parse(JSON.stringify())

JSON.parse() 可以将JSON字符串转换为 JavaScript 对象

JSON.stringify() 可以将JavaScript 对象转换为JSON字符串 

3、JQuery、lodash等库实现深拷贝。JQuery的extend方法、lodash的cloneDeep方法

浅拷贝的实现方式:

Object.assign()、arr.concat()、arr.slice()

参考:彻底讲明白浅拷贝与深拷贝深拷贝与浅拷贝的区别,实现深拷贝的几种方法深拷贝的终极探索

4、“==”和“===”的区别

==相等运算符,===严格相等运算符

对于基本数据类型,==转化为数值类型再进行比较;===只要类型不一致,直接返回false,同类型进行值的比较

 一个基本数据类型与一个引用数据类型,==(调用Object.prototype.valueOf()方法)将对象转化为原始类型的值,再进行比较;===直接返回false

两个引用数据类型,==和===是没有区别的,不是比较它们的值是否相等,而是比较它们是否指向同一个地址;

//除了==null之外,其他都一律用===
const obj={x:100}
if(obj.a==null){}
//相当于if(obj.a===null || obj.a===undefined){}

参考:js里==和===有什么区别

5、truely变量和falsely变量

1)!和!!

!可将变量转换成boolean类型,null、undefined和空字符串取反都为true,其余都为false。

const name="hello"

console.log(!typeof name==="object")//false

console.log(!typeof name==="string")//false

2)truely变量 !!a===true的变量,两步非运算为true的变量

falsely变量 !!a===false的变量,两步非运算为false的变量

//以下是falsely变量,除此之外都是truely变量
!!0===false
!!NaN===false  //NaN not a number
!!''===false
!!null===false
!!undefined===false
!!false===false

if语句和逻辑判断 

//truely变量
const a=true
if(a){}
const b=100
if(b){}
//falsely变量
const c=''
if(c){}
let d
if(d){}
console.log(10 && 0) //0
console.log('' || 'abc') //'abc'
console.log(!window.abc) //true

6、let、const和var的概念与区别、变量提升与暂时性死区

var和let区别:

1、作用域不同,let是块级作用域;var是函数作用域

2、let不能在声明之前访问变量;var可以

3、let不允许重复声明。即不允许在相同作用域内,重复声明同一个变量;var可以

 const声明一个只读的常量。一旦声明,必须马上赋值,常量的值不能改变。

  • let使用场景:变量,用以替代var。
  • const使用场景:常量、声明匿名函数、箭头函数的时候。

参考:let 和 const 命令

变量提升:变量可以在声明之前使用,值为undefined

暂时性死区:ES6 明确规定,如果区块中存在 let 和 const 命令,则这个区块对这些命令声明的变量从一开始就形成封闭作用域。只要在声明之前使用这些变量,就会报错。这种语法称为“暂时性死区”

变量的声明提前:适用var关键字声明的变量,会在所有的代码执行之前被声明(但是不会赋值);但是如果声明变量时不使用var关键字,则变量不会被声明提前

函数的声明提前:使用函数声明形式创建的函数function(function 函数(){}),它会在所有代码执行之前就被创建,所以我们可以在函数声明前来调用函数;使用函数表达式创建的函数,不会被声明提前,所以不能在声明前调用

参考:js函数声明提升与变量提升js中的变量提升和函数提升变量提升和函数提升

let randomValue=111
function getInfo(){
  console.log(typeof randomValue)//ReferenceError
  let randomValue="111"
}
getInfo()

8、变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构( Destructuring )。

参考:变量的解构赋值

9、Symbol概念及其作用、应用场景

Symbol是一种提供独一无二的值的数据类型。

参考:javascript中symbol类型的应用场景(意义)和使用方法

二、原型和原型链

1、原型和原型链、prototype与__proto__的关系与区别

原型是一个对象。prototype显式原型,__proto__隐式原型。

原型关系:

每个函数都有一个prototype属性

每个实例(对象)都有一个__proto__属性

实例的__proto__指向对应构造函数的prototype

执行规则——原型链

获取属性或执行方法时,先在自身属性和方法寻找,如果找不到则自动去__proto__寻找,如果还没有找到则继续向上,直到一个对象的原型对象为 null。这样一层一层向上查找就会形成一个链式结构,称为原型链。null 没有原型,并作为这个原型链中的最后一个环节。

为什么要有原型链?

为了实现继承。构造函数的所有实例都可以访问构造函数原型中的方法和属性。把共有的属性和方法统一存放到一个共享空间,避免重复声明,节省内存。

prototype和__proto__的区别

prototype是函数才有的属性;__proto__是每个对象都有的属性,但__proto__不是一个规范属性,只是部分浏览器实现了该属性,对应的标准属性是[[Prototype]]

注:大多数情况下,__proto__可以理解为“构造器的原型”,即__proto__===constructor.prototype。(通过Object.create()创建的对象不适用于此等式)

参考:javascript——原型与原型链最详尽的 JS 原型与原型链终极详解,没有「可能是」

2、class基本语法及继承

6种JavaScript继承方式:

1)原型链继承

使用父类实例对象作为子类原型对象。

缺点:所有子类实例对象共享父类构造函数的属性和方法

function Parent(){
    this.name='lisi'
    this.type=['html','css','js']
}
Parent.prototype.Say=function(){
    console.log(this.name)
}

function Son(){}
Son.prototype=new Parent()

var son1=new Son()
son1.type.push('vue')
son1.Say()//lisi
console.log(son1.type)//["html","css","js","vue"]
var son2=new Son()
console.log(son2.type)//["html","css","js","vue"]

2)借用构造函数继承

通过构造函数call方法进行继承

缺点:没有办法调用父级构造函数原型对象的方法

function Parent(name){
    this.name=name
    this.type=['html','css','js']
}
function Son(name){
    Parent.call(this,name)
}

var son1=new Son('zhangsan')
son1.type.push('vue')
console.log(son1)//Son{name:"zhangsan",type:["html","css","js","vue"]}
var son2=new Son('lisi')
console.log(son2)//Son{name:"lisi",type:["html","css","js"]}

3)组合继承(原型链 + 借用构造函数)—— 最常用的继承模式

调用了两次父类构造函数,父类的实例属性被复制了两份,一份放在子类原型,一份放在子类实例,而且最后子类实例继承自父类的实例属性覆盖了子类原型继承自父类的实例属性。

缺点:调用两次父级构造函数,一次是在创建子级原型的时候,另一次是在子级构造函数内部

function Parent(name){
    this.name=name
    this.type=['html','css','js']
}
Parent.prototype.Say=function(){
    console.log(this.name)
}

function Son(name){
    Parent.call(this,name)
}
Son.prototype=new Parent()

var son1=new Son('zhangsan')
var son2=new Son('lisi')
son1.type.push('vue')
son2.type.push('react')
console.log(son1)//Son{name:"zhangsan",type:["html","css","js","vue"]}
console.log(son2)//Son{name:"lisi",type:["html","css","js","react"]}

4)原型式继承

利用一个空的构造函数为桥梁,将一个对象作为原型创建新对象

function fun(obj){
    function Son(){}
    Son.prototype=obj
    return new Son()
}

var parent={
    name:'zhangsan'
}

var son1=fun(parent)
var son2=fun(parent)
console.log(son1.name)//zhangsan
console.log(son2.name)//zhangsan

5)寄生式继承

在原型式继承的基础上,在函数内部丰富对象

function fun(obj){
    function Son(){}
    Son.prototype=obj
    return new Son()
}

function createAnother(obj){
    var clone=fun(obj)
    clone.say=function(){
        console.log('hi')
    }
    return clone
}

var parent={
    name:'zhangsan',
    type:['html','css','js']
}

var son1=createAnother(parent)
var son2=createAnother(parent)
console.log(son1.say===son2.say)//false

6)寄生组合式继承(组合 + 寄生)—— 最完美的继承模式

function fun(son,parent){
    var clone=Object.create(parent.prototype)
    son.prototype=clone
    clone.constructor=son
}

function Parent(name){
    this.name=name
    this.type=['js','css','html']
}
Parent.prototype.say=function(){
    console.log(this.name)
}

function Son(name){
    Parent.call(this,name)
}

fun(Son,Parent)
var son1=new Son('zhangsan')
var son2=new Son('lisi')
son1.type.push('vue')
console.log(son1)//Son{name:"zhangsan",type:["js","css","html","vue"]}
console.log(son2)//Son{name:"lisi",type:["js","css","html"]}

参考:继承的实现方式与优缺点6种JavaScript继承方式及优缺点

参考:Class 的基本语法

3、new的原理

手写实现new

function createFunc(){
    var obj=new Object()
    var Con=[].shift.call(arguments)
    obj.__proto__=Con.prototype
    var ret=Con.apply(obj,arguments)
    //new一个实例的时候,如果没有return,就会根据构造函数内部this绑定的值生成对象,如果有返回值,就会根据返回值生成对象
    return typeof ret==='object'?ret:obj
}

function Person(name,age){
    this.name=name
    this.age=age
}

var person=createFunc(Person,'zhansgan',19)
console.log(person)

参考:JavaScript:手写一个new实现javascript中,new操作符的工作原理是什么?

4、new和Object.create的区别

var Base = function () {}
var o1 = new Base();
var o2 = Object.create(Base);
//new添加的属性是在自身实例下
var o1 = new Object();
o1.[[Prototype]] = Base.prototype;
Base.call(o1);
//Object.create()的作用是将传入的参数直接作为新生成对象的原型创造一个对象。创建对象的属性是在原型下面的
Object.create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};

三、作用域和闭包

1、作用域和作用域链、执行上下文

作用域代表某个变量合法的使用范围,分为全局作用域、函数作用域和块级作用域(ES6新增)。

全局作用域:直接编写在script标签中的JS代码都在全局作用域。全局作用域在页面打开时创建,在页面关闭时销毁。在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,由浏览器创建,我们可以直接使用。在全局作用域中,创建的变量都会作为window对象的属性保存,创建的函数都会作为window对象的方法保存。全局作用域中的变量都是全局变量,在页面任意部分都可以访问到。

函数作用域:调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的。在函数作用域中可以访问到全局作用域中的变量,在全局作用域中无法访问到函数作用域中的变量。

自由变量:一个变量在当前作用域没有定义,但是被使用了;向上级作用域一层一层依次寻找,直到找到为止;如果到全局作用域都没有找到,则报错XX is not defined。这种一层一层的关系,就是作用域链 

JS代码在执行前,JS引擎做一番准备工作,创建对应的执行上下文。包括全局执行行下文、函数执行上下文。

全局执行上下文只有一个,由浏览器创建,即window对象;函数执行上下文可存在无数个,每当一个函数被调用时都会创建一个函数上下文。同一个函数被多次调用,会创建一个新的上下文。

通过执行上下文栈管理这些上下文,JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内。执行栈具有LIFO的特性。

参考:深入理解JavaScript作用域和作用域链一篇文章看懂JS执行上下文

2、bind、call、apply的区别

  • 三者都可以改变函数的this对象指向。
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。
  • 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入。
  • bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行 。

手写bind函数

Function.prototype.bind1=function(){
    //将参数拆解为数组
    const args=Array.prototype.slice.call(arguments)//arguments函数的所有参数,列表形式。将列表转为数组
    //获取this(数组第一项)
    const t=args.shift()//执行完,args数组第一项去除
    //fn1.bind()中的fn1
    const self=this
    //返回一个函数
    return function(){
        return self.apply(t,args)
    }
}

function fn1(a,b,c){
    console.log('this',this)
    console.log(a,b,c)
    return 'this is fn1'
}

const fn2=fn1.bind1({x:100},10,20,30)
const res=fn2()
console.log(res)

参考:bind、call、apply的区别与实现原理call、apply和bind方法的用法以及区别

3、如何正确判断this?

this在各个场景中的取值是在函数执行时确定的,不是在函数定义时确定的。

1)普通函数、setTimeout函数、立即执行函数  this指向window

function fn1(){
    console.log(this)
}
fn1()//window

2)使用call、apply、bind  this指向指定的对象

function fn1(){
    console.log(this)
}

fn1.call({x:100})//{x:100}//call直接执行

const fn2=fn1.bind({x:200})//bind返回新的函数执行
fn2()//{x:200}

3)作为对象方法被调用(对象函数)  this指向该方法所属对象

const zhangsan={
    name:'zhangsan',
    sayHi(){
        //this即当前对象
        console.log(this)
    },
    wait(){
        setTimeout(function(){
            //this===window,setTimeout本身触发的执行
            console.log(this)
        })
    }
}
zhangsan.sayHi()
zhangsan.wait()

4)在class方法中调用(构造函数)  如果没有return,则this指向这个对象实例;如果存在return返回一个对象,则this指向返回的这个对象

class People{
    constructor(name){
        this.name=name
        this.age=20
    }
    sayHi(){
        console.log(this)
    }
}
const zhangsan=new People('张三')
zhangsan.sayHi()//zhangsan对象

5)箭头函数

箭头函数的取值取上级作用域的值

const zhangsan={
    name:'zhangsan',
    sayHi(){
        //this即当前对象
        console.log(this)
    },
    waitAgain(){
        //箭头函数的this和这的this一样
        setTimeout(()=>{
            //this当前对象
            console.log(this)
        })
    }
}
zhangsan.sayHi()
zhangsan.waitAgain()

参考:如何正确的判断this的指向?

4、闭包及其作用

作用域应用的特殊情况,有两种表现:

1)函数作为参数被传递

2)函数作为返回值被返回

自由变量的查找,是在函数定义的地方向上级作用域查找,不是在执行的地方!

//函数作为返回值
function create(){
    let a=100
    return function(){
        console.log(a)
    }
}
const fn=create()
const a=200
fn()
//结果为100


//函数作为参数被传递
function print(fn){
    const a=200
    fn()
}
const a=100
function fn(){
    console.log(a)
}
print(fn)
//结果为100

闭包就是能够读取其他函数内部变量的函数

1)可以读取函数内部的变量

2)让这些变量的值始终保持在内存中

缺点:

1)导致变量不会被垃圾回收机制回收

2)不恰当的使用闭包可能会造成内存泄漏的问题

内存泄露是指访问不到的变量,依然占居着内存空间,不能被再次利用起来。

参考:学习Javascript闭包(Closure)

5、JS的垃圾回收机制

js是单线程的,垃圾回收的过程会影响js的执行(GC 阻塞js执行)

参考:内存管理谈谈 JS 垃圾回收机制JavaScript变量回收原则/垃圾回收机制

四、异步

1、概念

单线程和异步

Js是单线程语言,只能同时做一件事。

浏览器和nodejs已经支持JS启动进程,如Web Worker。

Js和DOM渲染共用一个线程,因为JS可修改DOM结构。

异步基于callback函数进行

同步和异步的区别?

异步不会阻塞代码执行;同步会阻塞代码执行

异步应用场景

网络请求,如ajax图片加载

定时任务,如setTimeout

(CPU空闲)

2、EventLoop事件循环/事件轮询、宏任务与微任务

eventloop是异步回调的实现原理

异步(setTimeOut,ajax等)使用回调,基于event loop;DOM事件也使用回调,基于event loop

eventloop过程:

同步代码,一行一行放在Call Stack执行

遇到异步,会先“记录”下,等待时机(定时,网络任务等)。时机到了,就移动到Callback Queue

如果Call Stack为空(即同步代码执行完),执行当前的微任务,尝试DOM渲染,触发EventLoop

轮询查找Callback Queue,如果有则移动到Call Stack执行;如果没有,继续轮询查找(永动机一样)

宏任务和微任务

宏任务:setTimeout、setInterval、Ajax、DOM事件

微任务:Promise、async/await

微任务执行时机要比宏任务早。微任务在DOM渲染前执行,宏任务在DOM渲染后执行

参考:这一次,彻底弄懂 JavaScript 执行机制JavaScript 运行机制详解:再谈Event Loop

3、Promise(Promise A+规范、手撕Promise.all、Promise.race、Promise相关API和方法)

解决callback hell(回调地域)问题

回调地域即回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调函数执行的条件

回调地狱的缺点? 不便于阅读 / 不便于异常处理

Promise链式调用解决,Promise本身是同步的,它的then和catch方法是异步的

Promise状态

pending初始状态,既不是成功,也不是失败状态、resolved/fulfilled,操作成功完成、rejected,操作失败

pending -> fulfilled 或 pending -> rejected,变化不可逆

pending状态不会触发then和catch;fulfilled状态会触发后续的then回调函数;rejected状态会触发后续的catch回调函数

Promise的then和catch如何影响状态的变化

then和catch正常返回fulfilled状态的promise,里面有报错则返回rejected状态的promise

Promise.all和Promise.race

  • Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。Promise.all(iterable) 所有参数的状态都变为fulfilled,结果才为fulfilled。有一个被rejected,状态就变成rejected,此时第一个被reject的实例的返回值,会传递给回调函数。
  • Promise.race返回最快的promise的结果,不管结果本身是成功状态还是失败状态。

Promise.all和Promise.allSettled区别?参考:Promise.all和Promise.allSettled的区别

参考:理解和使用Promise.all和Promise.race手撕遵循 Promise/A+ 规范的 Promise

4、async/await

async/await是同步语法,彻底消灭回调函数。语法如下:

function loadImg(src){
    const p=new Promise(
        (resolve,reject)=>{
            const img=document.createElement('img')
            img.onload=()=>{
                resolve(img)
            }
            img.onerror=()=>{
                const err=new Error(`图片加载失败${src}`)
                reject(err)
            }
            img.src=src
        }
    )
    return p//Promise对象
}

const src1='image/star.jpg';
const src2='image/tx.jpg';

async function loadImg1(){
    const img1=await loadImg(src1)
    return img1
}

(async function(){
    const img1=await loadImg1()//await后面可以接async函数
    console.log(img1.height,img1.width)

    const img2=await loadImg(src2)//还可以接Promise对象
    console.log(img2.height,img2.width)
})()//匿名函数

async/await和Promise的关系

执行async函数,返回的是Promise对象

await相当于Promise的then(处理成功情况)

try catch捕获异常,代替了Promise的catch(处理失败情况)

async/await只是一个语法糖。JS还是单线程,还是得有异步,还是基于event loop。

//判断执行顺序
async function async1(){
    console.log('async1 start')  //2 重要
    await async2()//立刻执行async2(),然后执行await
    //await的后面都可以看作是异步回调call back里的内容
    //类似event loop,setTimeout(cb1)
    //setTimeout({function(){console.log('async1 end')}})
    //Promise.resolve().then(()=>{console.log('async1 end)})
    console.log('async1 end')  //5
}

async function async2(){
    console.log('async2')  //3 重要
}

console.log('script start')  //1
async1()//立刻执行async1()
console.log('script end')  //4
//同步代码执行完,启动异步代码
//判断执行顺序
async function async1(){
    console.log('async1 start') //2
    await async2()  
    //await后面作为异步回调内容,微任务
    console.log('async2 end') //6
}

async function async2(){
    console.log('async2') //3
}

console.log('script start') //1

setTimeout(function(){  //宏任务
    console.log('setTimeout')   //8
},0)

async1()

//初始化promise时,传入的函数会立刻被执行
new Promise(function (resolve){
    console.log('promise1') //4
    resolve()
}).then(function(){ //微任务
    console.log('promise2') //7
})

console.log('script end') //5,同步代码执行完毕

5、Generator及其异步方面的应用

参考:Generator函数异步应用

6、几种异步方式的比较(回调、Promise、Generator、async)

参考:Promise,Generator(生成器),async(异步)函数详解 JavaScript 3种异步方式(Promise/async/Generator)

7、setTimeout用作倒计时为何会产生误差?

setTimeout是一个异步的宏任务,当执行setTimeout时是将回调函数在指定的时间之后放入到宏任务队列。但如果此时主线程有很多同步代码在等待执行,或者微任务队列以及当前宏任务队列之前还有很多任务在排队等待执行,那么要等他们执行完成之后setTimeout的回调函数才会被执行,因此并不能保证在setTimeout中指定的时间立刻执行回调函数

参考:setTimeout 倒计时为什么会出现误差?

8、Iterator和for...of(Iterator遍历器的实现)

异步循环

参考:Iterator 和 for...of 循环

9、循环语法比较及使用场景(for、forEach、for...in、for...of)

参考:js中4种遍历语法比较

for...in 普通对象,数组得到key值(String类型)

for...of 数组得到value值,还可以遍历Set、Map

10、defer和async的区别

参考:defer 和 async 的区别

五、JS Web API

1、DOM(Document Object Model)

DOM本质是一棵树(将HTML文档表达为树结构)。应用广泛的vue和React框架,封装了DOM操作。

1)DOM节点操作

attribute和property区别

property DOM属性,使用 .属性名 或者 ['属性名']进行设置和获取。Property属性可以赋任何类型的值

attribute html属性(例如html中常用的id、class等),使用setAttribute()和getAttribute()进行设置和获取。attribute属性值只能是字符串

两者都有可能引起DOM重新渲染,尽量使用property

HTML:  
    <div id="div1" class="container">
        <p>一段文字1</p>
        <p>一段文字2</p>
        <p>一段文字3</p>
    </div>
    <div id="div2">
        <img src="image/img.jpg">
    </div>
    <script src="js/dom.js"></script>

js:
const div1=document.getElementById('div1')    //getElementById
console.log('div1',div1)

const divList=document.getElementsByTagName('div')    //getElementsByTagName
console.log('divList.length',divList.length)
console.log('divList[1]',divList[1])

const containerList=document.getElementsByClassName('container')  //getElementsByClassName
console.log('containerList.length',containerList.length)
console.log('containerList[0]',containerList[0])


const pList=document.querySelectorAll('p')  //返回文档中匹配指定 CSS 选择器的所有元素
const p1=pList[0]

//property修改DOM元素js变量属性
p1.style.width='100px'
console.log(p1.style.width)
p1.className='red'
console.log(p1.className)
console.log(p1.nodeName)
console.log(p1.nodeType)    //普通的DOM节点nodeType是1

//attr修改DOM元素节点属性
p1.setAttribute('style','width:100px;')
console.log(p1.getAttribute('style'))

2)DOM结构操作

新增/插入节点

获取子元素,获取父元素

删除子元素

HTML同上

js:
const div1=document.getElementById('div1')

//新建节点
const newP=document.createElement('p')
newP.innerHTML='this is newP'
//插入节点
div1.appendChild(newP)
//移动节点(对现有节点执行appendChild)
const p1=document.getElementById('p1')
div2.appendChild(p1)

//获取父元素 
console.log(p1.parentNode)
//获取子元素列表
console.log(div1.childNodes)
const div1ChildNodesP=Array.prototype.slice.call(div1.childNodes).filter(child=>{
    if(child.nodeType===1){//将text节点过滤掉
        return true
    }
    return false
})
console.log(div1ChildNodesP)

//删除节点
div1.removeChild(div1ChildNodesP[0])

3)DOM性能

DOM操作非常“昂贵”,避免频繁的DOM操作

1)对DOM查询做缓存

for(let i=0;i<document.getElementsByTagName('p').length;i++){
    //每次缓存 ,都会计算length,频繁进行DOM查询
}

const pList=document.getElementsByTagName('p')
const length=pList.length
for(let i=0;i<length;i++){
    //缓存length,只进行一次DOM查询
}

2)将频繁操作改为一次性操作

//性能差
const list=document.getElementById('list')
for(let i=0;i<10;i++){
    const li=document.createElement('li')
    li.innerHTML=`List item ${i}`
    list.appendChild(li)
}


//修改后
const list=document.getElementById('list')
//创建一个文件片段
const frag=document.createDocumentFragment()
//执行插入
for(let i=0;i<10;i++){
    const li=document.createElement('li')
    li.innerHTML=`List item ${i}`
    frag.appendChild(li)
}
//都完成之后,再插入DOM树中
list.appendChild(frag)

参考:JavaScript常用DOM操作方法和函数

2、BOM(Browser Object Model)

BOM 提供了操作浏览器的属性和方法

navigator 浏览器信息

screen 屏幕信息

location url信息

history 浏览历史

//navigator
const ua=navigator.userAgent
const isChrome=ua.indexOf('Chrome')
console.log(isChrome)

//screen
console.log(screen.width)
console.log(screen.height)

//location
console.log(location.href)  //整个网址
console.log(location.protocol)  //协议
console.log(location.host)  //域名
console.log(location.search) //查询参数
console.log(location.hash) //#后的内容
console.log(location.pathname) //路径

//history
history.back()
history.forward()

参考:BOM中各对象属性和方法

3、事件绑定、事件冒泡、事件代理(事件委托)

事件绑定

HTML:
<button id="btn1">按钮</button>

js:
const btn=document.getElementById('btn1')
btn.addEventListener('click',event=>{
    console.log('clicked')
})


//通用的事件绑定函数
function bindEvent(elem,type,fn){//elem绑定元素,type事件类型,click等,fn事件监听的回调函数
    elem.addEventListener(type,fn)
}

const btn1=document.getElementById('btn1')
bindEvent(btn1,'click',event=>{
    console.log(event.target)   //获取触发的元素
    event.preventDefault()  //阻止默认行为,例如链接默认行为是跳转,阻止默认行为
    alert('clicked')
})

事件冒泡和事件捕获

事件冒泡 基于DOM树形结构,事件顺着触发元素向上冒泡。

事件捕获 与事件冒泡相反,事件会从最外层document对象开始触发,直到最具体的元素。

HTML:
    <button id="btn1">一个按钮</button>
    <div id="div1">
        <p id="p1">激活</p>
        <p id="p2">取消</p>
        <p id="p3">取消</p>
        <p id="p4">取消</p>
    </div>
    <div id="div2">
        <p id="p5">取消</p>
        <p id="p6">取消</p>
    </div>


js:
const p1=document.getElementById('p1')
bindEvent(p1,'click',event=>{
    event.stopPropagation()  //阻止冒泡
    console.log('激活')
})

const body=document.body
bindEvent(body,'click',event=>{
    console.log('取消')
    // console.log(event.target)
})

事件代理

基于事件冒泡机制,管理某一类型的所有事件。

代码简洁,减少浏览器内存占用。但是不要滥用

HTML:
    <div id="div3">
        <a href="#">a1</a><hr>
        <a href="#">a2</a><hr>
        <a href="#">a3</a><hr>
        <a href="#">a4</a><hr>
        <button>加载更多...</button>
    </div>

js:
const div3=document.getElementById('div3')
bindEvent(div3,'click',event=>{
    //数量太多,不好每个都绑定,所以绑定到父元素上。在父元素上获取触发元素,拿到触发元素再做其他动作
    event.preventDefault()
    const target=event.target
    if(target.nodeName==='A'){
        alert(target.innerHTML)
    }
})
//改进后的通用的事件绑定函数
function bindEvent(elem,type,selector,fn){//elem绑定元素,type事件类型,click等,fn事件监听的回调函数
    if(fn==null){//==两等
        fn=selector
        selector=null
    }
    elem.addEventListener(type,event=>{
        const target=event.target
        if(selector){
            //代理绑定
            if(target.matches(selector)){
                fn.call(target,event)
            }
        }else{
            //普通绑定
            fn.call(target,event)
        }
    })
}

//普通绑定
const btn1=document.getElementById('btn1')
bindEvent(btn1,'click',function(event){
    event.preventDefault()  //阻止默认行为,例如链接默认行为是跳转,阻止默认行为
    alert(this.innerHTML)
})


//代理绑定
const div3=document.getElementById('div3')
bindEvent(div3,'click','a',function(event){//箭头函数变为普通函数,this指向问题
    //数量太多,不好每个都绑定,所以绑定到父元素上。在父元素上获取触发元素,拿到触发元素再做其他动作
    event.preventDefault()
    alert(this.innerHTML)
})

addEventListener和onClick()的区别

参考:onclick与addEventListener区别

4、ajax和跨域

1)Ajax的请求过程

Ajax请求的四个步骤:

a)创建XMLHttpRequest异步对象

var xhr;
if(window.XMLHttpRequest){ 
    //IE7+,Chrome,Firefox,Safari,Opera执行此代码 xhr=new XMLHttpRequest;
}else{
   //IE5,IE6执行该代码
   xhr=new ActiveXObject("Microsoft.XMLHTTP");
}

b)使用open方法与服务器建立连接

c)向服务器发送数据

//method请求的类型get、post;url文件在服务器上的位置;async false同步、true异步
//send()方法post请求时才使用字符串参数,否则不用带参数。
//async为true表示脚本会在 send() 方法之后继续执行,而不等待来自服务器的响应。
xhr.open(method,url,async);
xhr.send();
//post请求设置请求头的格式内容
xhr.open("POST","ajax_test.html",true);
xhr.setRequestHeader("content-type","application/x-www-form-urlencoded");
xhr.send("fname=Herry&lname=Ford");

d)在回调函数中针对不同的响应状态进行处理

//同步处理,直接在send()后面处理返回来的数据。responseText获得字符串形式的响应数据、responseXML获得XML形式的响应数据。
xhr.open("GET","http://www.runoob.com/try/ajax/demo_get.php",false);
xhr.send();
document.getElementById("mydiv").innerHTML=xhr.responseText;
//异步处理
xhr.onreadystatechange=function () {//接收到服务端响应时触发
     if(xhr.readyState==4&&xhr.status==200){
           document.getElementById("mydiv").innerHTML=xhr.responseText;
      }
}

整体代码如下:

//简易版本
function ajax(url,successFn){
    const xhr=new XMLHttpRequest()

    //xhr.open('POST','/login',false) 注释掉post请求
    xhr.open('GET',url,true)//true代表异步请求
    xhr.onreadystatechange=function(){//回调函数
        if(xhr.readyState===4){
            if(xhr.status===200){
                successFn(xhr.responseText)
            }
        }
    }

    // const postData={
    //     userName:'zhangsan',
    //     password:'xxx'
    // }
    // xhr.send(JSON.stringify(postData))
    xhr.send(null)//get请求,不用发送任何数据
}
//Promise形式
function ajax(url){
    const p=new Promise((resolve,reject)=>{
        const xhr=new XMLHttpRequest()
        xhr.open('GET',url,true)

        xhr.onreadystatechange=function(){
            if(xhr.readyState===4){
                if(xhr.status===200){
                    resolve(JSON.parse(xhr.responseText))
                }else if(xhr.status===404){
                    reject(new Error('404 not found'))
                }
            }
        }
        xhr.send(null)
    })
    return p    
}

const url='/data/test.json'
ajax(url)
.then(res=>console.log(res))
.catch(err=>console.log(err))

xhr.readyState:请求状态;

0: (请求未初始化)还没有调用send()方法

1: (载入)已调用send()方法,正在发送请求

2: (载入完成)send()方法执行完成,已经接收到全部响应内容

3: (交互)正在解析响应内容

4: (完成)响应内容解析完成,可以在客户端调用

xhr.status:响应状态码;

1XX、2XX、3XX、4XX、5XX

实际项目中ajax的常用插件:Jquery实现ajax、fetch、axios(框架中使用)对XMLHttpRequest API进行封装

参考:分析ajax请求过程以及请求方法

2)跨域:同源策略及跨域实现方式和原理

ajax请求时,浏览器要求当前网页和server必须同源(安全)

同源:协议、域名、端口三者一致

加载图片、css、js可以无视同源策略。<img/>可用于统计打点(PV、UV)可使用第三方统计服务;<link/>、<script>可使用CDN,CDN一般都是外域;<script>可以实现jsonp

跨域:所有的跨域,都必须经过server端允许和配合;未经server端允许就实现跨域,说明浏览器有漏洞,危险信号。

1)JSOP:原理<script>可绕过跨域限制,服务器可以任意动态拼接数据返回。

为什么<script>标签不受同源策略限制?

jsonp的script标签请求回来的资源与当前域是相同的域,因此不受同源策略的影响。

$.ajax({
    url:'http://localhost:8882/x-origin.json',
    dataType:'jsonp',
    jsonpCallback:'callback',
    success:function(data){
        console.log(data)
    }
})

手写jsonp

手写jsonp前端面试-跨域的解决方法 / 手写JSONP

2)CORS:服务器设置http header

客户端还需要设置吗?

response.setHeader('Access-Control-Allow-Origin','http://localhost:8011')
response.setHeader('Access-Control-Allow-Headers', 'X-Requested-With')
response.setHeader('Access-Control-Allow-Methods', 'PUT,GET,POST,DELETE,OPTION')
response.setHeader('Access-Control-Allow-Credentials', true)

3)postMessage:HTML5的API,可以支持跨域发送数据

4)websocket:全双工通信协议

5)Nginx反向代理,服务器和服务器之间没有同源策略的限制。

参考:前端多种跨域方式实现原理详解

5、存储Cookie、localStorage、sessionStorage

Cookie 本身用于浏览器和Server通信。以分号分割,key=value形式。借用来做本地存储,所以存在一些限制。1)存储大小最大4KB。2)http请求时需要发送到服务端,增加请求数量。3)只能用document.cookie=‘...’来修改,太过简陋。

localStorage和sessionStorage 专门为存储设计,最大可存5M;API简单易用,getItem\setItem;不会随着http请求发送出去

localStorage数据会永久存储,除非代码或手动删除。一般使用的比较多

sessionStorage数据只存在于当前会话,浏览器关闭则清空

三者区别

容量

API易用性

是否跟随http请求发送出去

6、其他

1)JS中的String、Math和Array方法

map函数以函数作为参数,这个函数参数作用在数组的每一个元素上。

function pow(x) {
    return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);

reduce函数是将数组的前两个元素做运算,将运算的结果再与第三个元素做运算,以此类推运算到最后一个元素得出最后的结果。

//[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)

var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
    return x + y;
});

 map与reduce返回的都是一个新的数组,并不是原来的数组。

参考:总结JS中string、math、array的常用的方法数组的常见API

2)类数组与数组的区别与转换

参考:js 数组和类数组的区别

3)数组扁平化

参考:5种方式实现数组扁平化

4)数组去重

参考:数组去重的方法

5) JavaScript中的arguments

参考:Arguments 对象

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

参考:普通函数和箭头函数的区别

7)函数柯里化及其通用封装

参考:JS深入基础之函数柯里化及其通用封装和高颜值方法详解JS函数柯里化

8)ES6新增特性

Symbol

let、const

解构赋值

箭头函数

异步Promise

迭代器(Iterator),新的遍历方式for...of循环

模块化  把代码进行拆分,方便重复利用

参考:ES6新增的一些特性

9)Set和Map数据结构

参考:Set和Map数据结构

10)异常

参考:前端中 try-catch 捕获不到哪些异常和错误

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值