数据类型
(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
| 标题 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变成window对象的属性 | √ | ||
| 变量提升 | √ | ||
| 暂时性死区 | √ | √ | |
| 重复声明 | 可重复声明 会覆盖 | 不可会报SyntaxError | 不可会报SyntaxError |
| for循环中的迭代变量 | 不产生迭代变量 保存循环结束后的值 | 产生迭代变量 | 产生迭代变量 |
| 可以修改值 | √ | √ |
变量提升:
var声明变量和function声明函数会在声明的时候提升到当前作用域的顶部
结果是:
赋值之前就可以访问到变量 值是undefined
函数在声明之前就可以调用执行
ps:
变量提升先于函数提升 也就是说function a()会把var a覆盖
函数形参的变量提升 fn(b){。。}等价于 fn(){var b=undefined}
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是一个类数组对象 键是数组下标 值是传的参数
作用域链:
作用域是程序中变量合法的使用范围。
作用域规定了如何查找变量,查找变量时从当前作用域中找,找不到就从父级作用域找,一直找到全局作用域,形成链表叫作用域链。
js中的作用域是静态作用域即词法作用域。
变量的查找取决于它的定义而不是执行。
this:
this指向当前函数执行上下文。
this的指向是动态的,取决于执行。
改变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
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或者用事件委托来统一绑定
数组和字符串相关
检验数组
instanceof Array 或 Array.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函数
- 创建一个空的实例对象
- 实例对象的__proto__隐式原型属性指向函数的prototype的显式原型属性
- 修改函数的this指向实例对象
- 执行函数,如果函数执行返回对象将其对象返回,否则返回实例对象
使用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是单线程语言。
浏览器是多进程的,多个会话之间互不影响。
事件循环机制
-
callback stack(回调栈):同步代码从上至下一行一行执行,执行完出栈。 -
Macrotask Queue(宏任务队列):script(整体代码)、 ajax、setTimeout、setInterval、Dom监听等 -
Microtask Queue(微任务队列): Promise的then回调、async await(实质就是Promise.then)、 Mutation Observer API、queueMicrotask
主线程只有一个,执行同步代码。异步代码交给浏览器单开的线程,维护着宏任务队列和微任务队列,它们可以有多个。
事件循环的完整过程:
- 当前脚本作为一个宏任务执行,
- 执行过程中同步代码直接执行,遇到异步代码,宏任务进入宏任务队列,微任务进入微任务队列
- 当前宏任务出列
- 检查微任务队列,执行微任务队列的任务至空
- 执行宏任务队列中的宏任务,如果宏任务中有微任务则加入微任务队列,直到执行完当前宏任务
- 每次执行下一个宏任务之前,都要确保微任务队列是空的
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.innerText和node.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
事件捕获:事件从最外层节点由外到内传给事件源
阻止冒泡: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
图片懒加载(手写)
如果给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
浏览器渲染
面试题:
- js的DOM渲染是单线程的,那渲染的过程是什么样的?浏览器渲染页面的过程?
- script标签,包含async属性的script标签,包含defer属性的script标签对文档渲染有啥影响?
- css是否阻塞页面的解析和渲染?css渲染会不会阻塞dom渲染,会不会阻塞dom树建立
- js会阻塞dom渲染吗,图片加载会阻塞dom渲染吗?
- Dom渲染是在事件循环机制哪里实现的
- JS加载阻塞DOM渲染问题,怎么解决
- 生成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.绘制
进行图层绘制
DOM树-CSS树-render树(渲染树)-渲染(生成布局+绘制)
blog.csdn.net/weixin_4582…
Chrome多进程浏览器架构
- 浏览器主进程:主要负责用户交互、界面交互、子进程管理等功能(只有一个浏览器主进程)。
- 网络进程:负责网络资源加载。(只有一个网络进程)
- 渲染进程:浏览器内核,JavaScript引擎V8都是运行在该进程中,负责将网络下载的HTML、JavaScript、CSS、图片等资源转化为交互的页面。(每个tab标签页对应一个)
- 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代码。
async:加载js代码时不阻塞dom解析,但加载完成后立即执行js代码。
结果:可能阻塞也可能不阻塞dom的解析
defer:延迟加载和执行js代码,等html全部解析完再开始加载和执行,且defer的优先级高于DOMContentLoaded事件 会执行完defer的脚本再触发DOMContentLoaded事件
DOMContentLoaded和Load
DOMContentLoadeddom解析完触发
Load所有资源(css js 图片)加载完触发
DOM渲染的优化(待完善...)
1.重绘和回流方面
GPU加速: transform:translate代替top left opacity
js: 对于 scroll 等事件进行防抖/节流处理。 使用变量缓存对敏感属性值(offset等)的计算 避免频繁改动使用style,采用修改class的方式