面试知识点回顾篇之JavaScript

152 阅读17分钟

前言

文章只做知识点的总结!

数据类型

  • 基础类型:Number,String,Boolean,null,undefined,symbol。在内存中占据固定的大小,保存在栈内存中
  • 引用类型:Object,array, function。保存在堆内存中,栈内存存储对象的变量标识符及在堆内存中的存储地址

数据类型检测方法

typeof: 能够快速区分基本数据类型。不能区分Object,array和null,都返回为object。

console.log(typeof 1); // number 
console.log(typeof true); // boolean 
console.log(typeof 'mc'); // string 
console.log(typeof Symbol) // function 
console.log(typeof function(){}); // function console.log(typeof console.log()); // function console.log(typeof []); // object 
console.log(typeof {}); // object 
console.log(typeof null); // object 
console.log(typeof undefined); // undefined

instanceof:适用于判断自定义类的实例对象,能区分array,function,object。基本数据类型不能判断number,string,boolean

console.log(1 instanceof Number); // false 
console.log(true instanceof Boolean); // false console.log('str' instanceof String); // false 
console.log([] instanceof Array); // true console.log(function(){} instanceof Function); // true console.log({} instanceof Object); // true

Object.prototype.toString.call:精准判断数据类型。写法繁琐,建议封装后使用。

var toString = Object.prototype.toString; console.log(toString.call(1)); //[object Number] console.log(toString.call(true)); //[object Boolean] console.log(toString.call('mc')); //[object String] console.log(toString.call([])); //[object Array] console.log(toString.call({})); //[object Object] console.log(toString.call(function(){})); //[object Function] console.log(toString.call(undefined)); //[object Undefined] console.log(toString.call(null)); //[object Null]

var/let/const

var没有块的概念,可以跨块访问,不能跨函数访问。let只能在块作用域里访问,不能跨块,跨函数访问。const定义常量,必须赋值,只能在块作用域访问且不能修改值。 var可以先使用后声明,存在变量提升。let和const必须先声明后使用 var允许重复声明同一个变量,let和const不允许声明同一个变量

数据存储方法有那些,区别是什么

数据存储的方式有vuex,localstorage、sessionstorage,cookie, session等 存储方式对比

  • vuex状态管理库:存储到内存,应用于组件的状态通信
  • localstorage(本地存储):大小5M,以文件的方法存储在本地,永久保存(不主动删除,则一直存在),主要用于不同页面之间的传值
  • sessionstorage(会话存储):大小5M,临时保存,仅在当前会话下有效,关闭页面或浏览器后被清除。,主要用于不同页面之间的传值
  • cookie(浏览器存储):最大4kb,浏览器关闭被清除。
  • session(服务器存储):保存到服务器

IE兼容问题

  • IE11使用get请求,造成在初次请求以后再也不进行请求了,会从缓存中获取数据,解决方法是使用post请求或在get请求参数添加一个随机数

js中this的五种情况

普通函数执行,this指向window 函数作为对象的方法被调用时,this指向该对象 构造器调用,this返回这个对象 箭头函数this绑定上层对象 基于apply,call,bind调用,可以指定调用函数的this指向。

promise

promise是异步编程的一种解决方案.比传统回调函数更合理,更强大. promise优点: 避免层层嵌套的回调地狱 promise缺点: 无法中途取消 如果不设置回调函数,无法获取处理后信息 当处于pending状态,无法得知目前进展到哪一个阶段

简述MVVM

什么是MVVM?

MVVM,即视图模型双向绑定,是model-view-viewModel的缩写,也就是把mvc中的controller演变成viewmodel。model表示数据模型。view表示UI组件,viewmodel是view和model层的桥梁。数据会绑定到viewmodel层并自动将数据渲染到页面中,视图变化时候会通知viewmodel层更新数据。以前是dom结构更新视图,现在是数据驱动视图。

MVVM优点

低耦合:视图可以独立于model变化和修改 可重用性:视图逻辑放在model里,多个view可以重用 独立开发:开发人员可以专注于业务逻辑和数据的开发(viewModel)

JavaScript 执行机制

内容参考来源

juejin.cn/post/684490…

JavaScript概念

  • JavaScript是一门单线程语言
  • 事件循环是JavaScript的执行机制
  • 单线程任务分为同步任务与异步任务
  • 任务队列分为宏任务与微任务

执行栈(调用栈)

  • 执行栈是存储有关正在运行的子程序的消息的栈,有时称控制栈,运行时栈,调用栈。
  • 调用栈是一个具有LIFO(后进先出)结构的堆栈,用于存储在代码执行期间创建的所有执行上下文
  • JavaScript只有一个调用栈,只能从堆栈顶部添加或删除
  • 所有的代码都会被放入栈中执行,执行栈是一条主线程 代码案例:
const second = () => {
  console.log('Hello there!')
}
const first = () => {
  console.log('Hi there!')
  second();
  console.log('The End')
}
first()

导图分析 image.png 文字分析

  • 创建一个全局执行上下文(main()表示)并将其推到调用堆栈的顶部
  • 遇到first()调用时,推到堆栈的顶部
  • 遇到console.log('Hi there!'),推到堆栈的顶部,打印完成时从堆栈中弹出
  • 遇到second()调用,推到堆栈的顶部
  • 遇到console.log('Hello there!'),推到堆栈的顶部,打印完成时从堆栈中弹出,second函数结束,从堆栈中弹出
  • 遇到console.log('The End'),推到堆栈的顶部,打印完成时从堆栈中弹出,first函数结束,从堆栈中弹出
  • 全部完成,全局执行上下文(main())从堆栈中弹出

同步任务与异步任务

  • 单线程任务分为同步任务与异步任务
  • 同步任务指的是在主线程上排队执行的任务,只有前一个执行完毕,才能执行后一个任务。
  • 异步任务是指不进入主线程,而进入event table的任务。异步任务又分为宏任务与微任务 导图如下: image.png 导图分析:
  • 首先判断是同步任务还是异步任务,同步任务进入主线程依次执行,异步任务进行event table。
  • 异步任务在event table中注册函数,当满足触发条件时,回调函数被推入Event Queue
  • 直到主线程空闲时,会去event Queue中查看是否存在有可执行的异步任务,有则推入主线程中执行

宏任务与微任务

宏任务

  • 整体代码script
  • setTimeout
  • setInterval
  • I/O
  • UI Rendering

微任务

  • Promise
  • process.nextTick(node环境)

执行机制顺序

同级下微任务优先于宏任务执行

  • 主线程任务
  • 微任务
  • 宏任务

事件循环

  • 整段代码作为宏任务进入主线程,形成一个执行栈
  • 同步任务进入主线程依次执行
  • 异步任务进入Event Table执行并注册函数,当异步条件满足时,Event Table会将指定宏任务函数移入宏任务队列,微任务函数移入到微任务队列。
  • js引擎存在monitoring process进程,持续不断检查主线程执行栈是否为空,当主线程内的任务执行完毕为空,会先去微任务队列读取对应函数,进入主线程执行,执行完毕后,再去宏任务队列读取对应函数,进入主线程执行开启第二轮循环。
  • 上述过程会不断重复,即事件循环 事件循环导图如下: 事件循环.png 代码如下:
console.log('1'); 
setTimeout(function() { 
 console.log('2'); 
  new Promise (function(resolve) { 
    console.log('4');
    resolve(); 
  }).then(function() {
    console.log('5') 
  })
}) 
new Promise(function(resolve) { 
 console.log('7'); 
 resolve(); 
}).then(function() { 
 console.log('8') 
}) 
setTimeout(function() { 
 console.log('9');
 new Promise(function(resolve) { 
   console.log('11'); 
   resolve(); 
 }).then(function() { 
   console.log('12') 
 }) 
})

分析如下:
第一轮循环

  • 整体script作为宏任务进入主线程
  • 遇到同步任务console,输出1
  • 遇到异步宏任务setTimeout,其回调函数分发到宏任务Event Queue中,标志为setTimeout1
  • new Promise直接执行后,又遇到同步任务console,输出7;遇到异步微任务then,其回调函数分发到微任务Event Queue中,标志为then1
  • 又遇到异步宏任务setTimeout,其回调函数分发到宏任务Event Queue中,标志为setTimeout2
  • 执行微任务then1,输出8
  • 第一轮循环后输出1,7,8 第二轮循环
  • 执行宏任务Event Queue的setTimeout1
  • 遇到同步任务console,输出2
  • new Promise直接执行后,又遇到同步任务console,输出4;遇到异步微任务then,其回调函数分发到微任务Event Queue,标志为then2
  • 执行异步微任务then2,输出5
  • 第二轮循环输出2,4,5 第三轮循环
  • 执行宏任务Event Queue的setTimeout2
  • 遇到同步任务console,输出9
  • new Promise直接执行后,又遇到同步任务console,输出11;遇到异步微任务then,分发到微任务Event Queue,标记为then3
  • 依次执行微任务then3,输出12
  • 第三轮循环输出:9,11,12 总的顺序:1,7,8,2,4,5,9,11,12

setTimeout

  • setTimeout(fn, 0)是指定fn回调函数在主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。即便主线程为空,0毫秒实际上达不到,根据html标准,最低是4毫秒
  • setTimeout(fn, 500)是指满足条件500毫秒后,会把回调事件fn放进宏任务Event Queue,再等到主线程执行完成,栈为空时才能执行
// 设置0毫秒
console.log('先执行')
setTimeout(() => {
  console.log('setTimeout执行了')
}, 0)
  • 输出先执行
  • 输出setTimeout执行了
// 设置1000毫秒
console.log('先执行')
setTimeout(() => {
  console.log('setTimeout执行了')
}, 1000)
  • 输出先执行
  • 1s后输出setTimeout执行了
// 设置500毫秒
console.log('先执行')
setTimeout(() => {
  console.log('setTimeout执行了')
}, 500)
// sleep函数需要执行1s才能执行完毕
sleep(1000)
  • 输出先执行
  • 执行sleep事件,1s后执行完毕
  • 输出setTimeout执行了(实际时间等待超过500毫秒)

setInterval

  • setInterval是指每隔指定的时间将回调函数移入Event Queue

写在最后

  • 遇到宏任务,需要把这个宏任务下的整个流程执行完毕,如setTimeout1,必须执行完里面的宏任务(包括里面的微任务),再执行新的宏任务

JavaScript内存空间

内容参考来源

juejin.cn/post/684490…

栈内存

  • 栈内存主要是用于存储各种基本类型的变量及引用类型变量的指针
  • 栈内存每个小单元大小基本相等
  • 系统效率较高

堆内存

  • 堆内存存储引用类型的变量
  • 堆内存大小不固定
  • 堆内存需要分配空间和地址,还要把地址存储到栈中,所以效率低于栈

变量存储机制

// 基本数据类型-栈内存 
let a1 = 0; 
// 基本数据类型-栈内存 
let a2 = 'this is string'; 
// 基本数据类型-栈内存 
let a3 = null; 
// 对象的指针存放在栈内存中,指针指向的对象存放在堆内存中 
let b = { m: 20 }; 
// 数组的指针存放在栈内存中,指针指向的数组存放在堆内存中 
let c = [1, 2, 3];

image.png

变量复制

基本类型变量

let a = 20; 
let b = a; 
b = 30; 
console.log(a); // 此时a的值是多少,是30?还是20?
  • a的值为20
  • a,b都是基本类型,存储在栈内存中,各自有独立的栈空间,修改b的值后,a的值不会发生变化

image.png

引用类型复制

let m = { a: 10, b: 20 }; 
let n = m; 
n.a = 15; 
console.log(m.a) //此时m.a的值是多少,是10?还是15?
  • m.a的值为15
  • m,n都是引用类型,栈内存中存放地址指向堆内存中的对象,引用类型的复制会为新的变量自动分配一个相同地址值,指向同一个对象,所以修改n.a,相应的m.a也发生改变

image.png

垃圾回收

  • 栈内存中变量一般在它当前执行环境结束就会被销毁被垃圾回收机制回收
  • 堆内存中的变量由于不确定其他地方是否还有对它的引用,堆内存中的变量只有在所有对它引用都结束后才会被垃圾回收机制回收

内存泄漏

文章参考:juejin.cn/post/698418… 当不再使用的对象内存,没有及时被回收时,就造成了内存泄漏。 造成内存泄漏的原因

  • 不正当使用闭包可能会造成内存泄漏(使用后及时置空)
  • 遗忘的定时器setInterval(使用后及时使用clearInterval进行清除)
  • 遗忘的事件监听器addEventListener(使用后及时使用removeEventListener进行清除)
  • 遗忘的监听者模式bus.bus.on(使用后进行bus.bus.off进行移除)
  • 未清除的console(及时清理console输出)

虚拟Dom和真实Dom

内容来源

juejin.cn/post/684490…

真实dom解析流程

image.png 所有浏览器渲染引擎工作流程大致分为下面5步:

  • 第一步,创建dom树。用html分析器分析html元素,构建一颗dom树。
  • 第二步,生成样式表。用css分析器分析css文件和元素上的inline样式,生成页面的样式表。
  • 第三部,构建render树。将dom树和样式表关联起来,构建一颗render树。每个dom节点都有attach方法,接受样式信息,返回一个render对象(renderer),这些render对象最终会被构建成一颗render树
  • 第四步,确定节点坐标。根据render树结构,为每个render树上的节点确定一个在显示屏上出现的精确坐标。
  • 第五步,绘制页面。根据render树和节点显示坐标,然后调用每个节点的paint方法,将他们绘制出来。

真实Dom缺陷

  • 原生js操作dom时,浏览器会从构建dom树开始从头到尾执行一遍流程。
  • 在一次操作中,需要更新10个dom节点,浏览器收到第一个dom请求后并不知道还有九次更新操作,因此会马上执行流程,最终执行10次。例如第一次计算完,接着下个dom更新请求,前个节点的坐标值就改变了,这样前一次计算为无用功。频繁操作dom代价昂贵,会出现页面卡顿,影响用户体验。

虚拟dom的好处

  • 虚拟dom是为了解决浏览器性能问题而设计出来的。
  • 若一次操作中有10次更新Dom的动作,虚拟dom不会立即操作Dom,而是将这10次更新的diff内容保存到本地一个js对象中,最终将这个js对象一次性attach到dom树上,再进行后续操作,避免大量的计算量。
  • js对象模拟dom节点好处是页面的更新可以先全部反映在虚拟dom上,操作内存中的js对象的速度显然要更快,等更新完成后,最终的js对象映射成真实dom,再交给浏览器去绘制。

虚拟dom算法实现

用js对象模拟dom树

真实dom节点

<div id="virtual-dom"> 
  <p>Virtual DOM</p> 
  <ul id="list"> 
    <li class="item">Item 1</li> 
    <li class="item">Item 2</li> 
    <li class="item">Item 3</li> 
  </ul>
  <div>Hello World</div>
</div>

原型与原型链

内容来源

zhuanlan.zhihu.com/p/72520189

面试回答

  • 原型:所有js对象都有一个__proto_属性,指向它的原型对象。如果要访问一个对象的属性但没有在该对象上找到时,它会找该对象原型以及该对象原型的原型,直到找到一个名字匹配或到达原型链末尾。
  • 原型链:由相互关联的原型组成的链状结构的就是原型链。

构造函数

var p1 = new Person('zs', '男', 'basketball')
  • 在JavaScript中,用new关键字来调用得函数,称为构造函数。
  • 构造函数首字母一般大写
  • Person就是构造函数

构造函数执行过程

function Person (name, gender, hobby) {
  this.name = name
  this.gender = gender
  this.hobby = hobby
  this.age = 6
}
var p1 = new Person('zs', '男', 'basketball')
  • 当以new关键字调用时,会创建一个新的内存空间,标记为Person的实例 image.png
  • 函数体内部的this指向该内存,给this添加属性,相当于给实例添加属性 image.png
  • 默认返回this,相当于默认返回该内存空间。即p1保存的地址为#f1,同时被标记为Person的实例

原型

  • 原型是一个可以被复制(或者叫克隆)的一个类,通过复制原型可以创建一个一模一样的新对象。通俗来说,原型就是一个对象模板。
  • 所有的引用类型(数组,对象,函数)都有__proto__(隐式原型)属性,属性值为普通对象。
  • 所有的函数除了有__proto__属性外还拥有prototype(原型)属性,属性值为普通对象。
  • 原型对象:每创建一个函数,函数会自动带有prototype属性,该属性是一个指针,指向一个对象,称为原型对象。

构造函数,实例对象,原型对象三者的关系

function Person (name, gender, hobby) {
  this.name = name
  this.gender = gender
  this.hobby = hobby
  this.age = 6
}
var p1 = new Person('zs', '男', 'basketball')
var p2 = new Person('zs2', '男2', 'basketball2')
  • 原型Person定义了一些公用的属性和方法。
  • 利用原型Person创建出来的新对象实例(p1和p2)会共享原型Person的所有属性和方法。
  • 实例对象p1只有__proto__属性。
  • 构造函数既有__proto__属性又有prototype属性。
  • proto__和prototype都是对象,表示他们都有__proto
  • 实例对象p1的隐式原型指向构造函数的显示原型,即p1.proto === Person.prototype。
  • 当调用某种方法或查找某种属性时,首先会在自身调用和查找,如果自身没有该属性或方法,则会去它的__proto__属性中调用查找,也就是它的构造函数的prototype。
  • 通过原型创建的新对象实例是相互独立的。
p1.getName = function(){
  console.log(this.name);
}
p1.getName();    // zs
p2.getName();   // undefined

原型链

  • 原型链是原型对象创建过程的历史记录。

原型继承

function Person (name, gender, hobby) {
  this.name = name
  this.gender = gender
  this.hobby = hobby
  this.age = 6
}
function Animal (address, weight) {
  this.address = address
  this.address = address
  this.text = '123'
}
Person.prototype = new Animal()
var p1 = new Person('zs', '男', 'basketball')
console.log(p1.text) // 123
  • 通过原型链的方式,实现Person继承Animal的所有属性和方法。
  • 当p1访问text时,先去p1实例找,找不到就去Person原型对象上找,由于原型对象已经指向Animal,当发现没有text属性,就去Animal的原型对象上找,找到后返回。

节流与防抖

节流

  • 在函数执行一次之后,在指定时间期限内不再工作,过了时间才生效。在规定时间内,函数只能被调用一次
function trottle (fun, delay) {
  let valid = true
  return function () {
    if (!valid) {
      return false
    }
    valid = false
    setTimer(() => {
       fun()
       valid = true
    }, delay)
  }
}
function showLoaction(){
  console.log(12, 'showLoaction')
}
window.onload = trottle(showLoaction, 1000)

防抖

  • 第一次触发事件时,不立即执行函数,而是给出一个期限值(如200ms)
  • 200ms内没有再次触发,则执行函数,否则,取消当前计时,重新开始计时
  • 在短时间内大量触发同一事件,只会执行一次函数
function debounce(fun, delay) {
  let timer = null
  return function () {
  	if (timer) {
  	  cleanTimeout(timer)
  	}
  	timer = setTimeout(fun, delay)
  }
}
function showLocation() {
  console.log(12, 'showLocation')
} 
window.onload = debounce(showLocation, 1000)

应用场景

  • 文本框搜索,用户不断输入时,可以用防抖只请求一遍资源
  • window触发resize,用户不断调整浏览器窗口时,可以使用防抖只触发一遍
  • 鼠标不断点击(mousedown)触发,可以用节流来执行事件
  • 监听滚动事件,判断是否滑到底部,可以用节流来判断

闭包

闭包就是[函数] + [函数内部能访问到的变量]的总和

var local = '变量' 
function foo() {
  console.log(local)
}

但是由于需要局部变量,因此需要函数套函数

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

优点:可以读取函数内部的变量,实现变量数据共享.

缺点:被引用的变量不能被销毁,造成内存泄漏。

解决方法:使用完变量后手动赋值为null.

手写深拷贝

call、apply、bind区别

  • 函数传参
call从第2-n个参数都是传给fun函数
apply把第2个数组参数传给fun函数
  • 执行
call与apply改变函数this上下文后马上执行该函数
bind是返回改变了this上下文后的函数,不执行该函数
  • 返回值
call、apply返回fun的执行结果
bind返回fun的拷贝,并指定了fun的this指向,保存了fun的参数

数组系列

数组方法

  • 添加元素,返回添加完后的数组长度
const arr = [1, 2, 3, 4, 5]
// 尾部添加
arr.push(6)
// 头部添加
arr.unshift(6)
  • 删除元素
const arr = [1, 2, 3, 4, 5]
arr.shift()

数组去重

Array.form(new Set(arr))