【八股】JavaScript

236 阅读19分钟

数据类型

(5+1)个基础类型: String Boolean Number Null Undefined +Symbol BigInt(es6)

+引用类型: Object(Array Function)

值类型 存在栈内存中,变量拿到的就是它的值

引用类型 存在堆内存中,变量拿到的只是它的一个引用,是它的地址

变量基础 变量开头只能是字母 $和_ 不能是数字

判断数据类型的方法

typeof 【除了null的基本类型 + function】

引用数据类型除了function 其他返回的都是object

instanceof 【引用类型】检测构造函数的显式原型属性是否出现在某个实例对象的原型链上

ps null instanceof Object返回false

Array.isArray() 【数组】

Object.prototype.toString.call()【可以判断所有数据类型 将对象转换为一个原始值】

不同的数据类型的原型对象重写了toString方法 所以要调用Object的原型方法 通过call修改this指向

let a = new Number(1)//通过构造函数返回Number实例对象
let b = Number(1)//通过Number函数形式 返回1 
let c = String(1)
console.log(c instanceof String)//返回false instanceof只能判断引用数据类型
console.log(c.__proto__ === String.prototype)//返回true

判断一个对象是否为空对象

1、用序列化转成字符串 判断是否是"{}"

2、Object.keys()返回键的数组 判断长度为0

3、Object.getOwnPropertyNames()方法获取对象的属性名数组 判断长度为0

4、for in 循环

let result = function (obj) {
  for (let key in obj) {
    return false;//如果不为空,可遍历,返回 false
  }
  return true;
}
console.log(result(obj));//返回true

==和===

相等和严格相等(全等)

== 允许在相等比较中进行隐式类型转换,而 === 不允许;

为了防止做隐式转换一般都用===,如果只判断一个变量值是否为null或者变量未定义,只需使用“==”即可。

a == null 等价于a === null || a === undefined

==:只比操作数 会有隐式的数据类型转换

  • 两个都为简单类型,字符串和布尔值都会转换成Number,再比较
  • 简单类型与引用类型比较,对象转化成其原始类型的值,再比较
  • 两个都为引用类型,则比较它们是否指向同一个对象
  • null 和 undefined 相等
  • 存在 NaN 则返回 false

===:全等,不做类型转换

    0==null//false
    +0===-0 //true
    undefined == false//false

var const let

标题varletconst
作用域函数作用域块级作用域块级作用域
变成window对象的属性
变量提升
暂时性死区
重复声明可重复声明 会覆盖不可会报SyntaxError不可会报SyntaxError
for循环中的迭代变量不产生迭代变量 保存循环结束后的值产生迭代变量产生迭代变量
可以修改值

变量提升

juejin.cn/post/693337…

var声明变量和function声明函数会在声明的时候提升到当前作用域的顶部

结果是:

赋值之前就可以访问到变量 值是undefined

函数在声明之前就可以调用执行

ps:

变量提升先于函数提升 也就是说function a()会把var a覆盖

函数形参的变量提升 fn(b){。。}等价于 fn(){var b=undefined}

image.png

function a(){ } 
var a ; 
console.log(typeof a); //function 
var f1 = function () { 
   console.log(1); 
} 
function f1 () {
   console.log(2); 
} 
f1() ;//1

作用域:

函数作用域vs块作用域 块作用域是{}代码块内

var的作用域是函数作用域 可以跨块访问 let不能跨块跨函数作用域访问

暂时性死区:

let和const在声明之前访问会报RefferenceError

const修改值:

修改对象的属性值没问题

修改基本变量类型会报TypeError

执行上下文

execution content (EC)

执行上下文定义了当前代码的执行环境,包括全局执行上下文和函数执行上下文。

全局上下文是最外层的上下文,在浏览器里是window对象。

当执行当前函数时,浏览器创建当前执行上下文对象,推入执行上下文堆栈的栈顶,执行完出栈,栈的最底部是默认的全局执行上下文。

执行上下文的重要属性:变量对象、作用域链、this

变量对象:Variable object(VO)

ps 激活对象AO就是激活了的VO 是一个东西

函数的arguments参数列表初始化一个变量对象,然后函数内部声明的变量函数将作为属性和方法添加到变量对象上。

arguments类数组对象

arguments是一个类数组对象 键是数组下标 值是传的参数

image.png

作用域链:

作用域是程序中变量合法的使用范围。

作用域规定了如何查找变量,查找变量时从当前作用域中找,找不到就从父级作用域找,一直找到全局作用域,形成链表叫作用域链。

js中的作用域是静态作用域即词法作用域。

变量的查找取决于它的定义而不是执行。

this:

this指向当前函数执行上下文。

this的指向是动态的,取决于执行。

juejin.cn/post/694602…

juejin.cn/post/684490…

改变this指向的方法:

1、箭头函数

箭头函数: 箭头函数定义的时候 this的指向就确定了 即外层的this

2、call apply bind

第一个参数都用来改this的指向

apply和call类似,直接执行该函数并改变this指向。apply传参数数组,call传一个个的参数。

bind返回新的函数

fun.apply(obj,[1,2])

fun.call(obj,1,2)

fun.bind(obj,1,2)()

多次通过bind绑定会指向第一个对象 后一个bind只能改变前一个bind的指向 最终指向还是第一个bind

垃圾回收

执行上下文的变量对象VO在执行上下文出栈后会被垃圾回收。

垃圾回收的两种方法:

1、标记清除 标记:从根节点遍历为每个可以访问到的对象都打上一个标记,表示该对象可达。 清除:在没有可用分块时,对堆内存遍历,若没有被标记为可达对象就将其回收。

2、引用计数

立即执行函数IIFE

声明一个匿名函数并立即执行(function(){})()

避免外界访问函数内变量,不会污染全局作用域;切断作用域链,防止闭包等内存污染。

闭包

闭包是一个函数,可以访问外部函数作用域内的变量。

函数嵌套函数,且内部函数存在对父级作用域变量的引用就会导致闭包。

因为变量被引用着,所以当另外一个函数执行结束时,变量并不会被回收,始终存在内存中。

    function a(){
        var i =1;
        return function(){
            console.log(i++);
        }
    }

    var b=a;
    var c1=a();
    var c2=b();
    c1()//1
    c1()//2
    c2()//1

使用场景:

1、循环中使用闭包解决 var 定义函数的问题

    for(var i=0;i<3;i++){
        (function(j){
            setTimeout(()=>{
            console.log(j);
        },j*1000)})(i)
    }

内部函数:箭头函数 外部函数:立即执行函数

i传给j j就是内部函数对外部函数的引用 从而形成闭包

2、防抖、节流(手写)

节流throttle:

函数在执行一次之后,超过规定时间才会执行第二次;

规定时间内只能触发一次,适用于多次事件平均分配时间来触发。

应用:

resize 窗口调整
scroll 页面滚动
mousemove DOM元素拖拽功能
click 疯狂点击

防抖debounce:

在规定时间内,只让最后一次生效,前面的不生效。

应用:多次事件只响应最后一次

搜索框实时联想input-keyup

image.png

3、模拟私有属性

将私有属性写在外部函数中 返回一个函数闭包 闭包返回一个对象 对象里面有访问私有属性的方法

    function getGeneratorFunc(){
        let _name = 'John'
        let _job = 'student'
        return function(){
            return {
                getName(){return _name},
                getJob(){return _job}
            }
        }
    }

    const obj = getGeneratorFunc()()
    console.log(obj.getName()) //John
    console.log(obj._name) //undefined

4、自定义bind

    Function.prototype.myBind = function(){
        if(typeof this !== "function") throw new Error()
        // 获取参数列表
        const args = [...arguments].slice(1)
        // 获取this
        let _this = [...arguments].shift()
        // 获取当前函数
        const selfFun = this
        return function(){
            return selfFun.apply(_this,args)
        }
    }

    function fn1(a){
        console.log(this)
        console.log(a);
    }
    fn1.bind({x:1},2)()

5、柯里化

闭包带来的问题:内存泄漏

内存泄露(对应内存已经不再被使用却没有释放)

内存泄露的可能原因:闭包的过度使用、全局变量的无意创建、DOM事件绑定后没有解绑

对应解决:避免过度用闭包、少用var多用let、销毁阶段解绑DOM或者用事件委托来统一绑定

数组和字符串相关

image.png

检验数组

instanceof ArrayArray.isArray()

question:forEach vs map..?

改变原数组的方法

删除/添加元素:

push(...items): 添加1/多个元素到数组的最后,返回【新数组的长度】

unshift(...items):添加元素到数组的开头,返回【新数组的长度】

pop(): 删除最后一个元素 返回【删除的项】

shift():删除第一个元素 返回【删除的项】

splice(index,num,...items):剪切,从index处删除num个元素,插入items。返回【新数组】

反转/排序:

reverse():反转数组,返回【反转后的新数组】

sort(compareFn):排序,比较函数可选。返回【排序后的新数组】

不改变原数组的方法

操作方法:

slice(start,end) 切片

concat(arr)拼接

flat(depth) 扁平化嵌套数组 默认depth为1

toString()返回用逗号连接各value的字符串

join(sep) 返回用sep连接各元素的字符串 默认不传用逗号连接

和字符串的split()相反 split是按分隔符分离为数组

搜索方法:

indexOf(item) lastIndexOf(item) includes(item) find(func) findIndex(func)

归并方法:

reduce() reduce((prev,curr)=>{...return ...} , prev)

迭代方法:

传参为映射函数,接受三个参(item, index, arr)=>{....}

filter()

every() 与,遍历每一个元素执行映射函数 直到有false返回

some() 或 遍历每一个元素执行映射函数 直到有true返回

map() 每个元素执行映射函数 返回映射后的新数组

forEach() 每个元素执行映射函数 没有返回值

ps forEach不能跳出循环 (不能用continue和break)想跳出可以用throw Error的方式

复制填充:

fill(value,start,end)从start到end填充value

Array.from 这个不是原型上的方法哦 从已有的数组(也可以是类数组)返回浅拷贝数组

ps 输出1-100的数组

Array.from(new Array(100),(item,index) => index+1)

new Array(100)返回长度为100 内容都为undifined

扩展运算符...

可以替代concat

[...arr1,arr2] 等价于arr1.concat(arr2)

深拷贝、浅拷贝(手写)

深拷贝:

拷贝所有属性且拷贝多层至基本数据类型,属性是对象时,会重新开辟内存空间存该对象。拷贝后的对象和源对象修改的话互不影响。

浅拷贝:

拷贝所有属性但只拷贝一层,属性是对象时,只简单拷贝其地址值。拷贝后的对象和源对象修改的话会相互影响。

深拷贝的实现:

简单版:用JSON.Stringify和JSON.parse

函数属性会丢失,循环引用会出错。

最终解决版:用map + 递归

浅拷贝的实现:

简单版:

Object.assign(target,obj1,obj2...)Array.prototype.concat(obj1,obj2...)

解构赋值解决一切 {...obj} [...arr]

JSON

本质是字符串,但数据结构和对象一样

全局对象window.JSON 两个常用方法

字符串的方法

slice()注意包左不包右

trim()去空格

split() 默认分割符是','

注意字符串虽然有length 有iterator 但是不能取索引 可以先转成数组再操作

正则表达式

原型和原型链

原型链

每个函数都有prototype显示原型属性,默认指向一个空对象,为显式原型对象。显式原型对象有constructor属性,指向相应函数。

每个实例对象都有隐式原型属性,指向相应的构造函数的显式原型。

实例对象.__proto__ === 相应构造函数对象.prototype

实例对象获取自身属性/方法时,先在自身找,找不到就沿着隐式原型链一直找, 形成一条隐式原型链,尽头是Object.prototype.__proto__ === null

其中, Function.__proto__ === Function.prototype

(所有构造函数包括Function都是Function的实例对象)

instanceof 手写

A instanceof B:

判断B的显式原型在不在A的隐式原型链上

obj instanceof Object true

obj instanceof Function false

fn instanceof Object true

fn instanceof Function true

pad上面有原型链终极图

手写new操作符

new操作符的原理,手写new函数

  1. 创建一个空的实例对象
  2. 实例对象的__proto__隐式原型属性指向函数的prototype的显式原型属性
  3. 修改函数的this指向实例对象
  4. 执行函数,如果函数执行返回对象将其对象返回,否则返回实例对象

使用new就不要在构造函数中 return

function _new(fn,...args){
        if(typeof fn !="function") throw new Error('fn应为函数')
        // 1.新建空对象
        let obj = {}
        // 2.对象的隐式原型指向构造函数的显式原型
        obj.__proto__ = fn.prototype
        // 改变this指向新对象
        let res = fn.call(obj,...args)
        // 如果构造函数有返回值且是object实例 返回该对象 否则返回新创建的对象
        return res instanceof Object? res :obj
    }

事件循环 event loop

进程 线程

理解:开一个应用程序相当于在操作系统中开一个进程,进程里包括多个线程或只有一个主线程。

JavaScript是单线程语言。

浏览器是多进程的,多个会话之间互不影响。

事件循环机制

面试题 juejin.cn/post/707909…

  1. callback stack(回调栈):同步代码从上至下一行一行执行,执行完出栈。

  2. Macrotask Queue(宏任务队列):script(整体代码)、 ajax、setTimeout、setInterval、Dom监听等

  3. Microtask Queue(微任务队列): Promise的then回调、async await(实质就是Promise.then)、 Mutation Observer API、queueMicrotask

主线程只有一个,执行同步代码。异步代码交给浏览器单开的线程,维护着宏任务队列和微任务队列,它们可以有多个。

事件循环的完整过程:

  1. 当前脚本作为一个宏任务执行,
  2. 执行过程中同步代码直接执行,遇到异步代码,宏任务进入宏任务队列,微任务进入微任务队列
  3. 当前宏任务出列
  4. 检查微任务队列,执行微任务队列的任务至空
  5. 执行宏任务队列中的宏任务,如果宏任务中有微任务则加入微任务队列,直到执行完当前宏任务
  6. 每次执行下一个宏任务之前,都要确保微任务队列是空的

DOM相关

DOM原生js操作

1、 增

var para=document.createElement("p"); 
var node=document.createTextNode("这是一个新段落。"); para.appendChild(node);

2、 删

得先找到他的父元素

var parent=document.getElementById("div1"); 
var child=document.getElementById("p1"); 
parent.removeChild(child);

3、 查

document.getElementById ()

document.getElementsByTagName ()

document.getElementsByCalssName()

document.querySelector()

document.querySelectorAll()

获取特殊元素:

body:document.body

html:document.documentElement

获取父、子、兄弟节点:

node.parentNode node.children node.nextElementSibling node.previousElementSibling

4、改

修改内容:node.innerTextnode.innerHTML

修改样式:node.style.fontSize

自定义属性

设置:直接在html标签里写data-name="tom" 或者用document.setAttribute('data-name','tom')

获取:element.dataset.name 去掉data前缀

事件相关(事件冒泡、事件捕获、事件委托/代理)

event对象(自己总结不一定对)

如果用在传参的时候用<div onclick = callback(event)>形式要传event过去 然后在定义callback的时候用一个形参接受

如果是在js代码里监听 可以在定义的时候直接在第一个参数用任意字符占位接受event事件

绑定事件:

1.on开头的事件

eventTarget.onclick = callback

<div onclick = callback()>

同一个元素同一个事件只能设置一个处理函数,最后注册的处理函数将会覆盖前面注册的处理函数

(onclick如果写在标签里要直接执行,详见防抖节流调用不成功的情况)

2.addEventListener事件监听

eventTarget.addEventListener('click',callback[, useCapture])

useCapture可选,默认是 false,事件冒泡时调用,true为事件捕获时调用

移除事件用removeEventListener

事件冒泡和事件捕获

事件冒泡:事件从事件源从内到外依次传给父节点直到window

事件捕获:事件从最外层节点由外到内传给事件源

image.png

阻止冒泡:e.stopPropagation()

事件委托/事件代理

利用事件冒泡的特性,将多个子元素的同类事件监听委托给(绑定在)共同的一个父组件上。

好处:减少内存占用(事件监听的回调变少),动态添加的内部元素也能响应

例子:导航栏 绑在ul上 判断e.target的类型 执行相应操作

offsetX pageX...

pageX: 页面X坐标位置 scrollTop+clientTop

pageY: 页面Y坐标位置

screenX: 屏幕X坐标位置

screenY: 屏幕Y坐标位置

clientX: 鼠标的坐标到页面左侧的距离

clientY: 鼠标的坐标到页面顶部的距离

clientWidth:可视区域的宽度

clientHeight:可视区域的高度

offsetX:鼠标坐标到元素的左侧的距离

offsetY:鼠标坐标到元素的顶部的距离

offsetLeft: 该元素外边框距离包含元素内边框左侧的距离

offsetTop:该元素外边框距离包含元素内边框顶部的距离

offsetWidth: width + padding-left + padding-right + border-left + border-right

offsetHeight: height + padding-top + padding-bottom + border-top + border-bottom

image.png

图片懒加载(手写)

如果给img标签的src写真实的url地址,会一次性把请求都发出去,等待时间很长

懒加载即按需加载,判断图片进入可视范围时,才进行加载请求。

实现方法:

初始化时 给src绑定一个小型图片 比如1px*1px的透明图

真实的url写在自定义属性data-src里

监听滚动事件 判断图片在视口内则将src替换为data-src中的真实url

1.滚动监听+getBoundingClientRect+innerHeight(手写)

element.getBoundingClientRect() 返回一个DOMRect矩形对象

相对于视口的左上角定位的left top right bottom值

+自身的width height(包括盒子模型的border和padding的)

弊端:触发重绘和回流

2.IntersectionObserver API

BOM相关

BOM:window document location navigator history screen

浏览器渲染

面试题:

  1. js的DOM渲染是单线程的,那渲染的过程是什么样的?浏览器渲染页面的过程?
  2. script标签,包含async属性的script标签,包含defer属性的script标签对文档渲染有啥影响?
  3. css是否阻塞页面的解析和渲染?css渲染会不会阻塞dom渲染,会不会阻塞dom树建立
  4. js会阻塞dom渲染吗,图片加载会阻塞dom渲染吗?
  5. Dom渲染是在事件循环机制哪里实现的
  6. JS加载阻塞DOM渲染问题,怎么解决
  7. 生成DOM树和CSSOM树之后怎么生成渲染树

浏览器渲染过程:

1.解析html 生成dom树

遇到img标签,对src的url地址立即发请求加载图片

dom是保存在内存中树状结构 可以通过js语句操作

script标签 display none的节点 注释 也会被添加

2.解析css 生成cssom(css object model)

背景图片不加载

3.将dom树和cssom合并为渲染树render树

渲染树只包括可见的节点

从dom树根部遍历 只包括可见节点(忽略head标签 display none的) 从cssom中查找节点相应的样式并应用 合成渲染树

4.布局

遍历渲染树,对每个节点进行位置和样式计算(大小、位置

第一次计算节点的大小和位置叫布局,之后每一次触发叫回流(重排)

5.绘制

进行图层绘制

image.png DOM树-CSS树-render树(渲染树)-渲染(生成布局+绘制) blog.csdn.net/weixin_4582…

Chrome多进程浏览器架构

  1. 浏览器主进程:主要负责用户交互、界面交互、子进程管理等功能(只有一个浏览器主进程)。
  2. 网络进程:负责网络资源加载。(只有一个网络进程)
  3. 渲染进程:浏览器内核,JavaScript引擎V8都是运行在该进程中,负责将网络下载的HTML、JavaScript、CSS、图片等资源转化为交互的页面。(每个tab标签页对应一个)
  4. gpu进程:如3d绘制(transform就是这个进程负责)等等。。。

渲染进程包括的线程

1.JS引擎线程:JavaScript引擎V8,负责处理JavaScript脚本程序。 (js是单线程,如果是多线程,两个线程对dom做了冲突修改不好处理)

2.GUI 渲染线程: 负责渲染浏览器界面,解析 HTML,CSS,构建render树,布局和绘制等。 (GUI渲染线程和js主线程是互斥的,js解析时渲染线程会被挂起)

3.事件触发线程:控制事件循环的,维护异步事件队列,包括宏任务和微任务队列

(还有单独的定时器线程 异步请求进程)

渲染过程

重绘和回流

回流(重排):当元素属性发生改变且影响布局时(宽度、高度、内外边距等),产生回流,相当于 刷新页面。

触发:

  • DOM元素的几何属性发生变化
  • 更改DOM树的结构
  • 获取一些特定属性的值:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollxxx、clientxxx

重绘:当元素属性发生改变且不影响布局时(背景颜色、透明度、字体样式等),产生重绘,相当于 不刷新页面,动态更新内容

回流必定重绘 重绘不一定回流

CSS、JS、DOM解析和渲染阻塞问题

DOM解析:拿到html之后就开始DOM解析,生成DOM树

DOM渲染:从rendertree开始渲染

CSS不阻塞DOM解析(css解析和dom解析是并行过程)

但CSS解析阻塞DOM渲染(渲染树的生成要等CSSOM生成)

补充知识1:浏览器解析DOM时,虽然会一行一行向下解析,但是它会预先同时加载具有引用标记的外部资源(例如带有src标记的<script>标签),而在解析到此标签时,则无需再去加载,直接运行,以此提高运行效率。

补充知识2:浏览器无法预先知道脚本的具体内容,因此在碰到<script>标签时,会触发页面渲染,确保<script>脚本内能获取到DOM的最新的样式。

script async和defer

解析html时,如果遇到script标签,会暂停dom解析过程,(gui渲染线程挂起,js引擎运行),加载并执行js代码。 image.png

async:加载js代码时不阻塞dom解析,但加载完成后立即执行js代码。

结果:可能阻塞也可能不阻塞dom的解析

image.png

image.png

defer:延迟加载和执行js代码,等html全部解析完再开始加载和执行,且defer的优先级高于DOMContentLoaded事件 会执行完defer的脚本再触发DOMContentLoaded事件 image.png

DOMContentLoaded和Load

DOMContentLoadeddom解析完触发

Load所有资源(css js 图片)加载完触发

DOM渲染的优化(待完善...)

1.重绘和回流方面

GPU加速: transform:translate代替top left opacity

js: 对于 scroll 等事件进行防抖/节流处理。 使用变量缓存对敏感属性值(offset等)的计算 避免频繁改动使用style,采用修改class的方式