JavaScript常见面试题汇总

309 阅读24分钟

一、数据类型

1. JavaScript有哪些数据类型,它们的区别?

JavaScript共有8种数据类型,分别是 String、Number、Boolean、Undefined、Null、Object、Symbol、BigInt。

(1) 基本数据类型
  • string
  • number
  • Boolean
  • null
  • undefind
  • symbol(es6新增的)
  • BigInt
(2) 引用数据类型
  • Object
(3) 区别

存储位置的不同,基本类型存在栈中,引用类型存在堆中

  • 基本数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。

2. 数据类型检测

(1)typeof 返回是一个字符串,常用于判断基本数据类型:String、Number、Undefined、Boolean、Function,但是对象和Null和数组返回Object。

console.log(typeof '');//string
console.log(typeof 1);//number
console.log(typeof true);//boolean
console.log(typeof undefined);//undefined
console.log(typeof function () { });//function
console.log(typeof {});//object
console.log(typeof null);//object
console.log(typeof [1,2,3]);//object
console.log(typeof NaN);//number
console.log(typeof console.log());//function

(2) instanceof

instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

只要在原型链上有构造函数都会返回true,使用instanceof检测不太准确。

var arr = [];
console.log(arr);
console.log(arr instanceof Array);//true
console.log(arr instanceof Object);//true

let Person = function(){};
let p1 = new Person();
console.log(p1 instanceof Person);//true

let s1 = new String('leaf');
console.log(s1 instanceof String);//true

let str = "ice";
console.log(str instanceof String);//false

(3) constructor

constructor这个属性存在构造函数的原型链上的属性,指向构造函数,可以通过直接访问查看构造函数上的__proto__直接查看所属类型。

var arr = [];
console.log(arr);
console.log(arr.constructor === Array);

(4) Object.prototype.toString.call( )

返回一个“[object XXX]”格式的字符串,XXX就是具体的数据类型,包括:

let  a = Object.prototype.toString.
console.log(a.call(null)); //[object Null]
console.log(a.call(undefined)); //[object Undefined]
console.log(a.call('str')); //[object String]
console.log(a.call([]));//[object Array]
console.log(a.call(true));//[object Boolean]
console.log(a.call(new Date()));//[object Date]
console.log(a.call(function (){}));//[object Function]
console.log(a.call(new Error()));//[object Error]
let myReg = /leaf/;
console.log(a.call(myReg));//[object RegExp]

3. 判断数组的方式有哪些

  • 通过instanceof做判断
if (arr instanceof Array === false) {
    console.log("非数组");
    return false
} 
return true
  • 通过ES6的Array.isArray()做判断
const a = [];
const b = {};
Array.isArray(a);//true
Array.isArray(b);//false
  • Object.prototype.toString.call( )
let arr = [];
if(Object.prototype.toString.call(arr) === "[object Array]"){
   console.log("我是数组");
  return true
}
console.log("我不是数组");
return false

  • 通过原型链做判断
obj.__proto__ === Array.prototype;

4. 如何判断一个空对象?

  • for...in遍历对象属性
  • 使用JSON.stringify
let obj = {};

if (JSON.stringify(obj) === "{}") {
   return false
 }
return true //非空对象,返回true

  • Object.keys( )
let obj = {};
console.log(Object.keys(obj));//[]

var obj = { foo: 'bar', baz: 42 };
console.log(Object.keys(obj));//["foo", "baz"]

  • 可以通过判断返回数据的长度来知道它是否为空
let obj = {};

if (Object.keys(obj).length === 0) {
  console.log("空对象");
  return false
}
return true

二、 Object.keys( )的用法:

顾名思义,返回一个数组,值是对象的key

var obj = { foo: 'bar', baz: 42 };
console.log(Object.keys(obj));//["foo", "baz"]

三、如何理解this关键字

  1. 方法中的this,指向调用方法的对象
  2. 全局环境下指向全局对象
  3. 全局函数中的this,指向全局对象
  4. 内部函数中的this,指向全局对象
  5. 事件中的this,指向触发事件的DOM对象
  6. 构造函数中的this,指向new创建的对象
  7. 箭头函数中的this,指向定义函数上下文的this
  8. 使用闭包,var获取dom的索引
    1. 直接输出this指向全局对象

image.png

2. 全局函数其实是window(全局对象)的方法

function fun(){
    console.log(this)
}
fun() // Window{}

3. this放在方法中,this指向调用这个方法的对象

let cat = {
    name:"猫猫",
    sayName(){
        console.log("我是" + this.name)  // 我是猫猫
        console.log(this)  // {name: '猫猫', sayName: ƒ}
    }
}
cat.sayName();

image.png

4. 事件中的this,指向触发事件的DOM对象

<button>按钮</button>
const btn = document.querySelector("button")
btn.onClick = function(){
    console.log(this); // <button>按钮</button>
}

5. 构造函数中的this,指向new创建的对象

new关键字做了什么: new会创建对象,将构造函数中的this指向创建出来的对象,指向小f。

构造函数 : 是用来创建对象的

function F(){ 
    this.name = '小明'
}

let f = new F();
console.log(f); // F {name: '小明'}
console.log(f.name); // 小明

箭头函数this指向, 三种说法

  1. 第一种. 普通函数,谁调用指向谁,箭头函数,在哪里定义指向谁。
  2. 第二种. 箭头函数外面指向谁就指向谁
  3. 第三种. 箭头函数中没有this
let cat = {
    name:"猫猫",
    sayName(){
        console.log('1', this) // {name: '猫猫', sayName: ƒ}
        setTimeout计时器是全局函数,所以this指向window
        setTimeout(function(){ 
             console.log('2', this)  // window ()
        }, 1000)

        setTimeout(()=> {
            console.log('3', this) // {name: '猫猫', sayName: ƒ}
        }, 1000)
    }
}
cat.sayName();

2、call、apply、bind

基本语法

call可以调用函数, call可以改变函数中this的指向

function fun(){
    console.log(1, this) // window
}  
fun.call();
function fun2(){
    console.log(2, this) // 2 {name: '喵喵'}
}
let cat = {
    name: '喵喵'
}
fun2.call(cat);// 这样this就指向了cat
let pig = {
    name: '佩奇'
}
let dog = {
    name: '旺财',
    sayName(){
        console.log("5 我是" + this.name)  // 是佩奇
    },
    eat(food, food2){
        console.log("6 我喜欢吃" + food + food2)
    }
}
dog.sayName.call(pig) 
//  call可以调用函数, call可以改变函数中this的指向
dog.eat.call(pig, 'call', '肉') // 6 我喜欢吃call肉

// call 的第一个参数是改变this的指向,往后的参数,是要传的参数
// call参数是以此后传,
// apply参数是数组,其它与call一样
dog.eat.apply(pig, ['apply', '肉']) // 6 我喜欢吃apply肉
// bind 
dog.eat.bind(pig, '辣椒', '肉') // 不会打印
// bind 不会调用这个函数,call会调用这个函数, bind会将一个函数作为返回值,返回来

let fun3 =  dog.eat.bind(pig, 'bind', '肉'); 
fun3(); // 6 我喜欢吃bind肉

区别

call、bind,参数是一个一个的,apply是一个数组

bind 不会立即执行,会将返回函数作为返回值,

3、call、apply、bind的实际应用

// 继承:子类可以使用父类的方法

function Animal(){

    // 所以这里的this指向小cat 2
    this.eat = function(){
        console.log('吃东西')
    }
}

function Bird(){
    this.fly = function(){
        console.log('飞翔')
    }
}

function Cat(){
    console.log(this)
    Animal.call(this) // this指向小cat 1
    Bird.call(this) 
    // 以上用call实现的多重继承
    this.sayName = function (){
        console.log('输出自己的名字')
    }
}
let cat = new Cat();
cat.eat(); // 吃东西
cat.fly();
cat.sayName();

4、js运行机制(一)单线程

js为什么是单线程

这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。

举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变

HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

什么是同步与异步问题,同步程序执行完成后,执行异步程序

异步:计时器(setTimout,setInterval)、ajax、读取文件等

console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
setTimeout(()=>{
    console.log(3)
}, 0)
setTimeout(()=>{
    console.log(4)
}, 0)
console.log(5)

输出结果: 1,5,2,3,4

setTimeout(()=>{
    console.log('我')
}, 0)
for (let index = 0; index < 5; index++) {
console.log(index)
}
setTimeout(()=>{
    console.log('爱')
}, 0)
setTimeout(()=>{
    console.log('吃')
}, 0)
setTimeout(()=>{
    console.log('肉')
}, 0)
console.log(5)

输出结果: 0,1,2,5,我,爱,吃,肉

5、js运行机制(二)事件循环

process.nexTick方法

process.nexTick 同步代码执行完成之后,异步代码执行开始之前

process.nexTick(() => {
    console.log(1)
})
console.log(2)
setTimeout(()=>{console.log(3) }, 0)
console.log(4)

输出结果是, 2、4、1、3

setImmediate方法

setImmediate 同步和异步代码执行之后,执行setImmediate

setImmediate(()=> {
    console.log(1)
})
process.nexTick(() => {
    console.log(2)
})
console.log(3)
setTimeout(()=>{console.log(4) }, 0)
console.log(5)

输出结果是, 3、5、2、4、1

什么是事件循环

javascript里面的任务有两种,同步任务和异步任务。

同步任务是指:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

异步任务指的是,不进入主线程、而进入任务队列的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

console.log("a");
setTimeout(function () {
    console.log("b");
},0);
console.log("c");

//a
//c
//b

js中代码从上往下执行,执行第一行代码的时候控制台输出a,执行到第二行代码的时候遇到了setTimeout函数,因为setTimeout函数是个异步函数,所以,浏览器会记住这个事件,添加到时间表中,之后把这个事件的回调函数入栈到任务队列中。而此时主线程程序继续往下运行,到了第五行:console.log("c"),执行这条,控制台输出c。这时候主线程空了,他会到任务队列里面去查找是否有可以执行的任务,有的话直接拿出来执行,没有的话会一直去询问,等到有可以执行的。

image.png

image.png 这张图片里面已经画出了js的事件循环的流程了。 流程:

  1. 所有同步任务都在主线程上执行,形成一个执行栈。
  2. 当主线程中的执行栈为空时,检查事件队列是否为空,如果为空,则继续检查;如不为空,则执行3;
  3. 取出任务队列的首部,压入执行栈;
  4. 执行任务;
  5. 检查执行栈,如果执行栈为空,则跳回第 2 步;如不为空,则继续检查;

6、js运行机制(三)宏任务与微任务

事件循环其实就是入栈出栈的循环。上面例子中说到了setTimeout,那setInterval呢,Promise呢等等等等,有很多异步的函数。但是这些异步任务又分宏任务(macro-task)微任务(micro-task)

宏任务包括:script setTimeout, setInterval, setImmediate, I/O, UI rendering。

微任务包括:process.nextTick, Promises, Object.observe, MutationObserver。

每一次事件循环(Event Loop)触发时:

  1. 执行完主执行线程中的任务也就是执行第一个宏任务(macro-task)任务,例如script任务。
  2. 取出微任务(micro-task)中任务执行直到清空。
  3. 取出宏任务(macro-task)中一个任务执行。
  4. 取出微任务(micro-task)中任务执行直到清空。
  5. 重复3和4。

其实promise的then和catch才是微任务microtask,本身的内部代码是同步任务。

执行顺序

  1. 同步程序
  2. process.nexTick
  3. 异步——微任务
  4. 异步——宏任务
  5. setImmediate 以上是一次事件循环,之后就是那个任务进来就执行那个

注意:

  1. 在浏览器浏览器和node中的执行不一样。
  2. 任务队列里面是“先入先出”的。
console.log('global')

for (var i = 1;i <= 5;i ++) {
  setTimeout(function() {
    console.log(i)
  },i*1000)
  console.log(i)
}

new Promise(function (resolve) {
  console.log('promise1')
  resolve()
 }).then(function () {
  console.log('then1')
})

setTimeout(function () {
  console.log('timeout2')
  new Promise(function (resolve) {
    console.log('timeout2_promise')
    resolve()
  }).then(function () {
    console.log('timeout2_then')
  })
}, 1000)

控制台输出:

image.png

7、js运行机制(四)promise和async

promise

promise是es6新增得一个对象,

怎么创建一个promise? new Prosmise() 就可以了

// 可以通过 new Promise() 创建 Promise, Promise括号中可以加一个函数,如下
let p = new Promise(()=> {  // 将new Promise赋值给p
    console.log('1') 
})

// p 就是Promise对象,Promise有个方法是then,所以p也有一个.then

p.then(()=>{
    console.log('2') 
})

控制台输出 1, 这时p.then是不会执行得,

Promise中有个函数resolve(), 只有调用resolve得时候,才会执行then

let p = new Promise((resolve)=> {
    console.log('1') 
    resolve()
})

p.then(()=>{
    console.log('2') 
})

控制台输出 1,2

在resolve中写入'hello, world', resolve传出来的值是then里的形参

let p = new Promise((resolve)=> {
    resolve('hello, world');
})

p.then((res)=>{ // resolve传出来的值是then里的形参
    console.log(res) 
})

控制台输出 hello, world

举个例子

axios的get方法是个Promise对象,

axios会将获取到的远程数据通过resolve(data)返回来,然后通过then拿到数据

axios.get('').then((res)=>{
    console.log(res)
})

async

async 返回值是promise对象

async function fun(){
    return '我是返回值'
}
let a = fun();

console.log(a)

控制台输出

image.png

怎么输出"我是返回值"呢?

async function fun(){
    return '我是返回值'
}
let a = fun();
 a.then(res=> {
        console.log(res)
 })

控制台输出

image.png

怎么直接输出"我是返回值"

function fun(){
    return new Promise((resolve)=>{
        resolve('我是返回值')
    })
}

8.JavaScript中对象属性的描述,可枚举,可配置,可重写

Object 是 JavaScript 的一种 数据类型 。它用于存储各种键值集合和更复杂的实体。Objects 可以通过 Object() 构造函数或者使用 对象字面量 的方式创建

object详情描述走你

9、闭包——经典使用场景和含闭包必刷题

image.png

闭包

了解闭包前先来了解一下上级作用域和堆栈内存释放问题。

上级作用域的概念

  • 函数的上级作用域在哪里创建创建的,上级作用域就是谁
var a = 10 
function foo(){
    console.log(a) 
} 
function sum() { 
    var a = 20 
    foo() 
} 
sum() 
/* 输出 10 /

函数 foo() 是在全局下创建的,所以 a 的上级作用域就是 window,输出就是 10

思考题
var n = 10
function fn(){
    var n =20
    function f() {
       n++;
       console.log(n)
     }
    f()
    return f
}

var x = fn()
x()
x()
console.log(n)
/* 输出
*  21
    22
    23
    10
/

稍微提个醒,单独的 n++ 和 ++n 表达式的结果是一样的

思路:fn 的返回值是什么变量 x 就是什么,这里 fn 的返回值是函数名 f 也就是 f 的堆内存地址,x() 也就是执行的是函数 f(),而不是 fn(),输出的结果显而易见

  • 关于如何查找上级作用域

参考:彻底解决 JS 变量提升的面试题

JS 堆栈内存释放

  • 堆内存:存储引用类型值,对象类型就是键值对,函数就是代码字符串。

  • 堆内存释放:将引用类型的空间地址变量赋值成 null,或没有变量占用堆内存了浏览器就会释放掉这个地址

  • 栈内存:提供代码执行的环境和存储基本类型值。

  • 栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉。

但栈内存的释放也有特殊情况:① 函数执行完,但是函数的私有作用域内有内容被栈外的变量还在使用的,栈内存就不能释放里面的基本值也就不会被释放。② 全局下的栈内存只有页面被关闭的时候才会被释放

闭包是什么

在 JS 忍者秘籍(P90)中对闭包的定义:闭包允许函数访问并操作函数外部的变量。红宝书上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。 MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。这里的自由变量是外部函数作用域中的变量。

概述上面的话,闭包是指有权访问另一个函数作用域中变量的函数

形成闭包的原因

内部的函数存在外部作用域的引用就会导致闭包。从上面介绍的上级作用域的概念中其实就有闭包的例子 return f就是一个表现形式。

var a = 0
function foo(){
    var b =14
    function fo(){
        console.log(a, b)
    }
    fo()
}
foo()

这里的子函数 fo 内存就存在外部作用域的引用 a, b,所以这就会产生闭包

闭包变量存储的位置

直接说明:闭包中的变量存储的位置是堆内存。

  • 假如闭包中的变量存储在栈内存中,那么栈的回收 会把处于栈顶的变量自动回收。所以闭包中的变量如果处于栈中那么变量被销毁后,闭包中的变量就没有了。所以闭包引用的变量是出于堆内存中的。

闭包的作用

  • 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
  • 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化

闭包经典使用场景

  1. return 回一个函数
var n = 10
function fn(){
    var n =20
    function f() {
       n++;
       console.log(n)
     }
    return f
}

var x = fn()
x() // 21

这里的 return ff()就是一个闭包,存在上级作用域的引用。

  1. 函数作为参数
var a = '林一一'
function foo(){
    var a = 'foo'
    function fo(){
        console.log(a)
    }
    return fo
}

function f(p){
    var a = 'f'
    p()
}
f(foo())
/* 输出
*   foo
/ 

10、对象拷贝

js内存结构

11、防抖与节流

相同: 在不影响客户体验的前提下,将频繁的回调函数,进行次数缩减.避免大量计算导致的页面卡顿.

不同: 防抖是将多次执行变为最后一次执行,节流是将多次执行变为在规定时间内只执行一次.

防抖

防抖:用户触发时间过于频繁,只要最后一次事件的操作

指触发事件后在规定时间内回调函数只能执行一次,如果在规定时间内触发了该事件,则会重新开始算规定时间。

应用场景

  • debounce

    • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
    • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
    • 多次点击提交
  • throttle

    • 鼠标不断点击触发,mousedown(单位时间内只触发一次)

    • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

    <input type="text">
    
    let inp = document.querySelector("input")
    let t = null
    inp.oninput = function(){
        if(t !== null)
            clearTimeout(t)
        }
        t = setTimeout(()=>{
            console.log(this.value)
        }, 500)
    }
     
     
     // 以上代码封装
     
     inp.oninput = debounce(function(){
         console.log(this.value)
     }, 500)
     
     function debounce(fn, delay){
         let t = null;
        return function(){
            if(t !== null)
                clearTimeout(t)
            }
            t = setTimeout(()=>{
                fn.call(this);
            }, delay)
        }
     }

节流

节流:控制执行次数 作用:控制高频事件执行次数

<button id="throttle">点我节流!</button>

 <script>
   window.onload = function() {
     // 1、获取按钮,绑定点击事件
     var myThrottle = document.getElementById("throttle");
     myThrottle.addEventListener("click", throttle(sayThrottle));
   }

   // 2、节流函数体
   function throttle(fn) {
     // 4、通过闭包保存一个标记
     let canRun = true;
     return function() {
       // 5、在函数开头判断标志是否为 true,不为 true 则中断函数
       if(!canRun) {
         return;
       }
       // 6、将 canRun 设置为 false,防止执行之前再被执行
       canRun = false;
       // 7、定时器
       setTimeout( () => {
         fn.call(this, arguments);
         // 8、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
         canRun = true;
       }, 1000);
     };
   }

   // 3、需要节流的事件
   function sayThrottle() {
     console.log("节流成功!");
   }

 </script>

12、Promise

是什么

  • Promise 是一个对象,从它可以获取异步操作的消息

Promise的三种状态

  • pending(进行中)
  • fulfilled(已成功)
  • rejected(已失败)

状态的缺点

  • 无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。

  • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。

  • 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

实例方法

  • then()
  • catch()
  • finally()

13、null与undefind区别

null

表示没有对象,是空值

  • typeof null = Object
  • 转为数值为0
  • 作为函数的参数,表示该函数的参数不是对象;作为对象原型链的终点
null >= 0; //true 

null <= 0; //true 

null == 0; //false 

null > 0; //false 

null < 0; //false

null === 0; //false

undefined

  • 未定义,变量有声明,但是没赋值

  • typeof undefined = undefined

  • 转为数值为NaN

  • 访问对象上不存在的属性或者未定义的变量

let tut  = {}
console.log(tut.title) //undefined

  • 声明一个变量,但没有赋值
let b;
console.log(b); //undefined
  • 函数定义了形参,但没有传递实参
function updateTut(){
    console.log(tut)
}
updateTut()//undefined

14、js中哪些操作会内存泄漏

  • 忘记声明的局部变量
function a(){
    b=2
    console.log('b没有被声明!')
}

b 没被声明,会变成一个全局变量,在页面关闭之前不会被释放.使用严格模式可以避免.

  • 闭包带来的内存泄漏

闭包中引用,不会被回收

var leaks = (function(){
    var leak = 'xxxxxx';// 闭包中引用,不会被回收
    return function(){
        console.log(leak);
    }
})()
  • 定时器中的内存泄漏,定时器没有被清楚

15、NaN 是什么,用 typeof 会输出什么?

Not a Number,表示非数字,typeof NaN === 'number'

16、数组去重

【方法一】 es6 Set结构

let arr = ["2", "3", "5", "2", "8", "7", "0", "3", "5"];
  let set = new Set(arr);
 console.log(set) // {'2', '3', '5', '8', '7', …}
  类数组(set)转化为真正的数组的方法:
  
1let result = [...set]; // ["2", "3", "5", "8", "7", "0"]
  /* 或者 */
 2let result_1 = Array.from(set); // ["2", "3", "5", "8", "7", "0"]

17、DOM事件冒泡、事件捕获和事件委托

例子

<div id="outer">
    <p id="inner">Click me!</p>
</div>

事件流

事件流描述的是从页面中接收事件的顺序

事件冒泡

事件会从最内层的元素开始发生,一直向上传播,直到document对象。

因此上面的例子在事件冒泡的概念下发生click事件的顺序应该是

p -> div -> body -> html -> document

事件捕获

与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。

上面的例子在事件捕获的概念下发生click事件的顺序应该是

document -> html -> body -> div -> p

事件委托

事件委托,通俗的说就是利用事件冒泡机制,将元素的事件委托给它的父级或者更外级的元素处理。

事件委托的优点

  • 只需要将同类元素的事件委托给父级或者更外级的元素,不需要给所有的元素都绑定事件,减少内存占用空间,提升性能。
  • 动态新增的元素无需重新绑定事件

事件冒泡和事件捕获过程图:

image.png

1-5是捕获过程,5-6是目标阶段,6-10是冒泡阶段;

18、# 浏览器的回流与重绘 (Reflow & Repaint)

写在前面

在讨论回流与重绘之前,我们要知道:

  1. 浏览器使用流式布局模型 (Flow Based Layout)。
  2. 浏览器会把HTML解析成DOM,把CSS解析成CSSOMDOMCSSOM合并就产生了Render Tree
  3. 有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
  4. 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。

一句话:回流必将引起重绘,重绘不一定会引起回流。

回流 (Reflow)

Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见DOM元素
  • 激活CSS伪类(例如::hover
  • 查询某些属性或调用某些方法

一些常用且会导致回流的属性和方法:

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • scrollIntoView()scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

重绘 (Repaint)

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

性能影响

回流比重绘的代价要更高。

有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。

现代浏览器会对频繁的回流或重绘操作进行优化:

浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。

当你访问以下属性或方法时,浏览器会立刻清空队列:

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • widthheight
  • getComputedStyle()
  • getBoundingClientRect()

因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。

如何避免

CSS

  • 避免使用table布局。
  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolutefixed的元素上。
  • 避免使用CSS表达式(例如:calc())。

JavaScript

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

20、forEach、map、for区别

forEach

没有返回值

var a = [1,2,3,4,5] 
var b = a.forEach((item) => { item = item * 2 }) 
console.log(b) // undefined

21、js中哪些操作会内存泄漏

忘记声明变量

不正当的闭包

遗忘的定时器

22、 for...in和for...of的区别

  • for…of 遍历获取的是对象的键值
  • for…in 获取的是对象的键名;
  • for...in 循环主要是为了遍历对象

for...of 值

Object.prototype.sayHello = function(){
    console.log('Hello');
}
var myObject = {
    name:'zhangsan',
    age:10
}

for(let key of myObject){
    consoloe.log(key);
}
//输出结果
//typeError

Array.prototype.sayHello = function(){
    console.log("Hello");
}
var myArray = [1,200,3,400,100];
for(let key of myArray){
    console.log(key);
}
//输出结果
1,200,3,400,100


for...in 键名(key)

for… in 会遍历对象的整个原型链

//for in 应用于数组
Array.prototype.sayHello = function(){
    console.log("Hello")
}
Array.prototype.str = 'world';
var myArray = [1,2,10,30,100];
myArray.name='数组';

for(let index in myArray){
    console.log(index);
}
//输出结果如下
0,1,2,3,4,name,str,sayHello

//for in  应用于对象中
Object.prototype.sayHello = function(){
    console.log('Hello');
}
Obeject.prototype.str = 'World';
var myObject = {name:'zhangsan',age:100};

for(let index in myObject){
    console.log(index);
}
//输出结果
name,age,str,sayHello
//首先输出的是对象的属性名,再是对象原型中的属性和方法,
//如果不想让其输出原型中的属性和方法,可以使用hasOwnProperty方法进行过滤
for(let index in myObject){
    if(myObject.hasOwnProperty(index)){
        console.log(index)
    }
}
//输出结果为
name,age
//你也可以用Object.keys()方法获取所有的自身可枚举属性组成的数组。
Object.keys(myObject)


23、如何使用for...of遍历对象

如果需要遍历的对象是类数组对象,用Array.from转成数组即可。

var obj = {
    0:'one',
    1:'two',
    length: 2
};
obj = Array.from(obj);
for(var k of obj){
    console.log(k)
}

24、数组的原生方法有那些

  • 数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。

  • 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。

  • 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。

  • 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。

  • 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。

  • 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法

  • 数组归并方法 reduce() 和 reduceRight() 方法

25、数组的遍历方法有哪些

  • forEach()否数组方法,不改变原数组,没有返回值

  • map()否数组方法,不改变原数组,有返回值,可链式调用

  • filter()否数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用for...of否for...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环

  • every() 和 some()否数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回

  • false.find() 和 findIndex()否数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值

  • reduce() 和 reduceRight()否数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作

26、forEach和map方法有什么区别

这方法都是用来遍历数组的,两者区别如下:

  • forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
  • map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;