预解析
function fn(a,c){
console.log(a) //f a(){}
var a = 123
console.log(a) //123
console.log(c) //f c(){}
function a(){}
if(false){
var d=678
}
console.log(d)//undefined
console.log(b)//undefined
var b =function() {}
console.log(b)//f(){}
function c(){}
console.log(c)//f c(){}
}
fn(1,2)
//预编译
//作用域的创建阶段 预编译阶段
//预编译的时候做了哪些事情
//js的变量对象 AO对象 供js引擎自己去访问的
//1 创建了ao对象 2 找形参和变量的声明 作为ao对象的属性名 值为undefined 3 实参和形参相统一 4找函数 声明 会覆盖变量的声明
AO:{
a:undefined 1 function a(){}
c:undefined 2 function c(){}
d:undefined
b:undefined
}
- 词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)
JS为什么是单线程
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
JS的事件循环机制
-
js中的异步操作比如fetch setTimeout setInterval压入到调用栈中的时候里面的回调函数会进入到消息队列中去,消息队列会等到调用栈清空之后再执行
-
promise async await的异步操作的时候会加入到微任务中去,会在调用栈清空的时候立即执行,调用栈中加入的微任务会立马执行(微任务:Promise process.nextTick)
下列代码执行
setTimeout(()=>{
console.log(1)
},0)
setInterval(() => {
console.log('interval')
}, 1000);
new Promise(resolve=>{
console.log(2)
resolve()
}).then(res=>{
console.log(3)
})
new Promise(resolve=>{
console.log(5)
resolve()
}).then(resolve=>{
console.log(6)
})
console.log(4)
/*
2
5
4
3
6
1
interval
*/
执行优先级 调用栈>微任务队列>消息队列
哪些是异步任务
- setTimeout 和 setInterval
- DOM事件
- Promise
- 网络请求
- I/O
JS语言特性
- 运行在客户端浏览器上
- 不用预编译,直接解析执行代码(有预解析)
- 是弱类型语言,较为灵活
- 与操作系统无关,跨平台的语言
- 脚本语言,解释性语言
JS数据类型
基本数据类型:
字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol
引用数据类型:
对象(Object)、数组(Array)、函数(Function)。
前端事件流
事件捕获阶段
处于目标阶段
事件冒泡阶段
arguments
function get(){
console.log(arguments)
}
get(1,2,3)
//arguments是一个类数组对象
//可以通过Array.prototype.slice.call(arguments)来将其转化为数组
//也可以通过es6中的展开运算符来转化为数组
//for
为什么在调用这个函数的时候,代码中的b会变成全局变量
function func(){
let a=b=3
}
func()
console.log(b)//3
console.log(a)//error
因为let a=b=3等价于let a=(b=3) b并没有声明,所以在创建的时候会变成全局变量,在func()执行之后,因为b为全局变量,所以可以访问,但是a在func()的作用域内,外部并不能访问
this指向问题
this指向有哪几种
-
默认绑定:全局环境中this默认绑定到window
-
隐式绑定:被直接对象所包含的函数调用时,即方法调用时,this隐式绑定到该对象上
-
隐式丢失:被隐式绑定的函数丢失绑定对象时,会默认绑定到window
var name='window' var person={ name:'zs', getName:function(){ return this.name } } console.log(person.getName())//zs let getName2=person.getName console.log(getName2())//window -
显示绑定:通过call()、apply()、bind()方法把对象绑定到this上
-
new绑定:函数或方法调用前带有关键字new,就构成构造函数调用,对于this绑定来说,称为new绑定
在函数中直接使用
function get(content){
console.log(content)
}
get('您好')//可以看作下列语句的语法糖
get.call(window,'您好')
函数作为对象的方法被调用(谁调用 就指向谁)
var person = {
name:'张三',
run:function(time){
console.log(`${this.name}在跑步 最多${time}min就不行了`)
}
}
person.run(30)//可以看作下列语句的语法糖
person.run.call(person,30)
例题
var name=222
var a ={
name:111,
say:function(){
console.log(this.name)
}
}
var fun = a.say
fun() //fun,call(window) 222
a.say()//a.say.call(a) 111
var b={
name:333,
say:function(fun){
fun()
}
}
b.say(a.say)//传入之后等价于 fun()=a.say 等价于第一种 222
b.say = a.say
b.say()//b调用 等于console.log(this.name)复制到b的say方法中 即333
箭头函数中的this
- 箭头函数中的this是在定义函数的时候绑定的,而不是在执行函数的时候绑定
- 箭头函数中,this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为他没有this,所以也就不能用作构造函数
var x =11
var obj={
x:22,
say:()=>{
console.log(this.x)
}
}
obj.say() //输出是11
var obj={
birth:1990,
getAge:function(){
var b=this.birth
var fn=()=>new Date().getFullYear()-this.birth
return fn()
}
}
console.log(obj.getAge())//输出是2021-1990=31
因为箭头函数在getAge()中定义的,其父级为obj内部作用域,所以其箭头函数中的this指向的是父级obj对象,this.birth即1990
改变this指向的三种方法
- call
- apply
- bind
let obj = {
name:'胡图图'
}
function Child(name){
this.name = name
}
Child.prototype = {
constructor:Child,
showName:function(){
console.log(this.name)
}
}
var child = new Child('小美')
child.showName()//小美
child.showName.call(obj)//胡图图
child.showName.apply(obj)//胡图图
let bind = child.showName.bind(obj)//胡图图
bind()
call和apply会立即执行,而bind不会立即执行,bind除了返回是函数以外,它的参数和call是一样的。call传参是以逗号分割开,而apply传参则是一个数组
let arr = [1,2,3,4,5,6]
console.log(Math.max.call(null,1,2,3,4,5,6))//6
console.log(Math.max.call(null,arr))//NaN
console.log(Math.max.apply(null,arr))//6
JS中的深浅拷贝
赋值:当我们把一个对象赋值给一个新的变量时候,赋的其实是该对象在栈中的地址,而不是堆中的数据,也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此两个对象是联动的
浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响
深拷贝:从堆中开辟一块新的区域存放对象,原始对象和新对象之前互不影响
\
| 操作 | 和原数据是否指向同一对象 | 第一层数据为一般数据类型 | 第一层数据不是一般数据类型 |
|---|---|---|---|
| 赋值 | 是 | 改变会使得原始数据改变 | 改变会改变原始数据 |
| 浅拷贝 | 否 | 改变不会改变原始数据 | 改变会改变原始数据 |
| 深拷贝 | 否 | 改变不会改变原始数据 | 改变不会改变原始数据 |
-
赋值代码
var person = { name:"张三", hobby:['学习','敲代码','吃瓜'] } var person1 = person person1.name = "李四" person1.hobby[0]='玩耍' console.log(person)//都是name:"李四" hobby:['玩耍','敲代码','吃瓜'] console.log(person1)
-
浅拷贝代码(基本数据类型不受影响,引用数据类型被同时改变)
var person = { name:"张三", hobby:['学习','敲代码','吃瓜'] } function shallowCopy(obj){ var target = {} for(var i in obj){ if(obj.hasOwnProperty(i)){ target[i]=obj[i] } } return target } var person1 = shallowCopy(person) person1.name='王二' person1.hobby[0]="玩耍" console.log(person)//张三 玩耍 敲代码 吃瓜 console.log(person1)//王二 玩耍 敲代码 吃瓜 -
深拷贝代码(拷贝前后数据互不影响)
var person = { name:"张三", hobby:['学习','敲代码','吃瓜'] } function deepClone(obj){ var cloneObj = new obj.constructor() if(obj===null) return obj if(obj instanceof Date) return new Datr(obj) if(obj instanceof RegExp) return new RegExp(obj) if(typeof obj !== 'object') return obj for(var i in obj){ if(obj.hasOwnProperty(i)){ cloneObj[i]=deepClone(obj[i]) } } return cloneObj } // var person1 = JSON.parse(JSON.stringify(person))//大多数时候可以,但是如果对象中有日期、正 则表达式、函数就不行 var person1 = deepClone(person) person1.name="胡图图" person1.hobby[0]="打麻将" console.log(person1)//二者完全不影响 console.log(person)
浅拷贝的实现方式:
- lodash里面的_.cloneObj
- ...展开运算符
- Array.prototype.concat()
- Array.prototype.slice()
深拷贝的实现方式:
- JSON.parse(JSON.stringify()) 在对象中有正则表达式、Date对象、正则对象、Promise时会发生异常
- 递归实现(如上图中代码)
- loadsh里面的cloneDeep()
- jquery.extend()
闭包
什么是闭包?闭包有什么用?
闭包:能够访问其他函数作用域中的变量的函数
应用:模仿块级作用域;保存外部函数的变量;封装私有变量(单例模式)
闭包与堆内存:闭包中的变量并不保存在栈内存中,而是保存在堆内存中,所以函数调用之后闭包还能引用到函数内的变量
防抖
可以采用闭包的方式
var input = document.querySelector('input')
function debounce(delay){
let timer
return function(value){
clearTimeout(timer)
timer=setTimeout(function(){
console.log(value)
},delay)
}
}
var debounceFunc = debounce(1000)
input.addEventListener('keyup',function(e){
debounceFunc(e.target.value)
})
采用闭包的方式,return的函数中既可以使用timer,timer只会在调用debounce的时候生成一次
节流
一段时间内只做一件事情,无论点击多少次,等到执行完了之后再执行第二个事情,闭包解决节流
document.querySelector('button').addEventListener('click',thro(handle,2000))
function thro(func,wait){
let timeout
return function(){
if(!timeout){
timeout = setTimeout(function(){
func()
timeout=null
},wait)
}
}
}
function handle(){
console.log(Math.random())
}
闭包的底层原理
function a(){
var aa=123
function b(){
var bb = 234
console.log(aa)
}
return b
}
var res = a()
res()//能够输出123
因为a()执行完毕之后a对应的作用域链会断开,但是b在定义的时候就可以访问到a作用域内的作用域链,这个作用域链并不会断开,所以res()依然可以访问到aa=123,即可以输出123,闭包可以让变量一直保存在内存中。
闭包实现单例模式
var createLogin = function () {
var div = document.createElement('div')
div.innerHTML = "我是弹出的div"
div.style.display = 'none'
document.body.appendChild(div)
return div
}
var getSingle = function(fn){
var result
return function(){
return result || (result=fn.apply(this,arguments))
}
}
var create = getSingle(createLogin)
document.querySelector('button').onclick=function(){
var loginLay = create()
loginLay.style.display = "block"
}
无论点击多少次,只创建一个div
哪些操作会造成内存泄漏
-
闭包
-
意外的全局变量
function fn(){ a=1//a就是由于失误未进行var或者let声明,会被自动var提升到全局变量 } function fn(){ this.a=1//this会指向window,就变成了window下的a变量,这个只有在关闭页面的时候才会销毁 } -
被遗忘的定时器
-
脱离dom的引用(比如var div=document.querySelector('div')之后将其删除,但是并未对消除div这个引用,在内存中依然保留了对于div的引用)
var elements = { txt: document.getElementById("test") } function fn() { elements.txt.innerHTML = "1111" } function removeTxt() { document.body.removeChild(document.getElementById('test')); } fn(); removeTxt() console.log(elements.txt) //依然会输出<div id="test">1111</div>
高阶函数
将函数作为参数或者返回值的函数
function highOrder(params,callback){
return callback(params)
}
数组操作
数组扁平化
-
数组自带的扁平化方法
const arr = [1,[2,[3,[4,5]]],6] console.log(arr.flat(Infinity)) -
正则加JSON.parse(JSON.stringify(arr))
let arr=[1,[2,[3,[4,[5]]]]] let newarr=JSON.parse('['+JSON.stringify(arr).replace(/[|]/g,'')+']') console.log(newarr) -
递归
const array=[] const fn=(arr)=>{ for(let i=0;i<arr.length;i++){ if(Array.isArray(arr[i])){ fn(arr[i]) } else{ array.push(arr[i]) } } }
- reduce
arr.reduce(function(prev,cur,index,arr){})
arr 表示将要使用的原数组
prev 表示上一次调用的返回值或者初始值init
cur 表示当前正在处理的数组元素
index 表示当前正在处理的数组的索引,若提供init值,则索引为0,否则索引为1
init表示初始值
let arr=[1,[2,[3,[4,[5]]]]]
const fn=(arr)=>{
return arr.reduce((pre,cur)=>{
return pre.concat(Array.isArray(cur)?fn(cur):cur)
},[])
}
let newarr=fn(arr)
console.log(newarr)
数组去重
Set去重
let arr=[1,1,2,3,4,4,5,6]
let newarr=[...new Set(arr)]
console.log(newarr)
数组的IndexOf去重
let arr=[1,1,2,3,4,4,5,6]
function unique(arr){
let newarr=[]
for(var item of arr){
if(newarr.indexOf(item)==-1){
newarr.push(item)
}
}
return newarr
}
console.log(unique(arr))
reduce使用
计算数组中每个元素出现的个数
let person=['胡图图','胡图图','牛爷爷','壮壮']
let nameNum = person.reduce((pre,cur)=>{
if(cur in pre){
pre[cur]++
}else{
pre[cur]=1
}
return pre
},{})
console.log(nameNum)
数组去重
var arr = [1,2,3,4,5,1,2,3,4,5]
let newArr = arr.reduce((pre,cur)=>{
if(pre.includes(cur)){return pre}
else{return pre.concat(cur)}//此处如果用push pre会变成1
},[])
console.log(newArr)
数组遍历
for(let i=0;i<arr.length;i++){
}
for(var i in arr){//i是索引
arr[i]
}
for(var value of arr){//value是遍历的每一项的值
value
}
arr.map((item)=>{})//返回一个新数组
arr.forEach((item)=>{})//没有返回值
构造函数创建对象
ES5创建对象的三种方式
利用new Object()创建对象
var obj = new Object()
利用对象字面量创建对象
var obj = {}
利用构造函数创建对象
function Star(name,age){
this.name = name
this.age = age
this.sing = function(){console.log('我会唱歌')}
}
var ldh = new Star('刘德华',12)//构造函数不同于普通函数,要使用new
采用new之后this才会指向实例对象,构造函数里面不需要return
通过this创建的叫做实例成员,只有实例才能访问
不通过this创建的叫做静态成员,可以通过Star直接访问,不能通过实例去访问
function Star(name,age){
this.name = name
this.age = age
this.sing = function(){console.log('我会唱歌')}
}
Star.sex='男'
Star.play=function(){
console.log('打麻将')
}
console.log(Star.sex)
Star.play()
let ldh = new Star('刘德华',12)
console.log(ldh.sex)//undefined
ldh.play()//报错
原型链
先来看看以下代码
function Star(name,age){
this.name = name
this.age = age
this.sing = function(){
console.log('我会唱歌')
}
}
var ldh = new Star('刘德华',12)
var zxy = new Star('张学友',34)
console.log(ldh.sing===zxy.sing)//结果是false
说明创建两个对象,开辟了两个不同的内存空间,相同的部分仍然占用了不同的内存空间,造成了内存浪费
采用console.dir(Star)
Star有一个prototype属性,指向的是一个对象,即原型对象。所以可以考虑将公共的部分加到构造函数的原型对象上去,改写如下
function Star(name,age){
this.name = name
this.age = age
}
Star.prototype.sing = function(){console.log('我会唱歌')}
var ldh = new Star('刘德华',12)
var zxy = new Star('张学友',34)
console.log(ldh.sing===zxy.sing)//结果是true
//可以通过以下方式使用这个sing函数
ldh.sing()
Star.prototype.sing()
对象原型
但是sing()是加在Star.prototype上面的,ldh为啥可以访问呢,这就引出了 __ proto __ 了,我们打印ldh看一下
ldh有一个 __ proto __ ,对象都会有一个 __ proto __ 属性,它指向的是构造函数的prototype原型对象,即ldh的 __ proto __ 指向了Star的prototype,因此就可以使用挂载到Star的prototype上面的sing()方法了
可以通过以下代码来测试以下两者是否相等,结果为true,也验证了以上的说法
console.log(ldh.__proto__===Star.prototype)//true
用图说话直观一点(推荐b站pink老师,这一块讲的特别清晰,图片也是截取于他的视频中)
Star中prototype指向中的construtor
console.log(Star.prototype)
console.log(ldh.__proto__)
constructor顾名思义,就是构造函数,用以记录Star本身,constructor只要用于记录该对象引用与哪个构造函数,它可以让原型对象重新指向原来的构造函数
再考虑以上情况,假如我们要在prototype上挂载很多公共属性,采用Star.prototype.xxxx=xxxx的形式来说明显就非常的繁琐,于是考虑将我们要添加的封装成一个对象
Star.protoype={
sing:function(){},
movie:function(){},
play:function(){}
}
但是采用这种方法添加的话,我们的对象会直接覆盖prototype原有的一些属性,也无法知道它是引自哪个地方,所以这个时候constructor的作用就体现出来了
Star.protoype={
constructor:Star,//手动添加constructor将其指向重新指回Star
sing:function(){},
movie:function(){},
play:function(){}
}
构造函数实例和原型对象三角关系
原型链
console.log(Star.prototype.__proto__===Object.prototype)//true
console.log(Object.prototype.__proto__)//null
组合继承(普通属性用call改变this 方法用prototype加new)(构造函数继承加原型对象继承)
function Parent(name){
this.name = name
}
Parent.prototype.getName = function(){
return this.name
}
function Child(name){
Parent.call(this,name)
}
Child.prototype = new Parent()
Child.prototype.construcor = Child
const parent = new Parent('胡英俊')
const child = new Child('胡图图')
console.log(parent.name)//胡英俊
console.log(child.name)//胡图图
console.log(parent.getName())//胡英俊
console.log(child.getName())//胡图图
组合式继承的缺点(每一次创建Child实例对象的时候都要new一个Parent的实例对象)
寄生组合(child.prototype上加东西会影响parent.prototype)
//Child.prototype = new Parent()
Child.prototype = Parent.prototype
解决方案,浅拷贝
Child.prototype = Object.create(Parent.prototype)
对象字面量代替switch方法
const fruitsColor={
red:['apple'],
yellow:['banana']
}
function printFruits(color){
return fruitsColor[color] || []
}
console.log(printFruits('y'))
JS类型判断
-
typeof
typeof可以识别出基本类型boolean,number,undefined,string,symbol。但是不能识别null,null会返回object类型。对于引用类型,会把array、object统一归为object类型,但是可以识别出function。
-
instanceof
从结果中看出instanceof不能识别出基本的数据类型 number、boolean、string、undefined、null、symbol。但是可以检测出引用类型,如array、object、function。
-
Object.prototype.toString.call
此方法可以相对较全的判断js的数据类型。
什么是立即执行函数?有什么特点?有什么用处?
(function(){
//执行语句
})()
特点:立即执行函数中的代码,又不会在内存中留下对该函数的引用,函数内部的所有变量都会被立即销毁(除非这些变量赋值给了包含作用域中的变量)
作用:实现块级作用域
JS垃圾回收机制
JS垃圾回收机制主要是由一个叫垃圾收集器(garbage collector)GC的后台进程负责监控、清理对象、并及时回收空闲内存。
GC的最主要职责是监控数据的可达性,“可达性”的值就是哪些以某种方式可以访问或可用的值。比如直接访问或者链式访问等,当没有入口去访问它的时候,这块数据就变成了垃圾会被清除掉(一般来说没有被引用的对象就是垃圾,就是要被清除的,有个例外,几个对象相互引用形成一个环,但是其实根本没有访问入口,这几个对象也是垃圾)
如何清除垃圾
-
标记清除
function func3 () { const a = 1 const b = 2 // 函数执行时,a b 分别被标记 进入环境 } func3() // 函数执行结束,a b 被标记 离开环境,被回收 -
引用计数
function func4 () { const c = {} // 引用类型变量 c的引用计数为 0 let d = c // c 被 d 引用 c的引用计数为 1 let e = c // c 被 e 引用 c的引用计数为 2 d = {} // d 不再引用c c的引用计数减为 1 e = null // e 不再引用 c c的引用计数减为 0 将被回收 } 引用计数的缺点(循环引用)
function func5 () { let f = {} let g = {} f.prop = g g.prop = f // 由于 f 和 g 互相引用,计数永远不可能为 0 }像这种情况我们需要手动将变量的内存释放
f.prop = null g.prop = null在现代浏览器中,JavaScript使用的方式是标记清除,所以我们无需担心循环引用
==与===,Object.is的区别
console.log(""==0)//true
console.log("0"==0)//true
console.log(123=="123")//true
console.log(null==undefined)//true
console.log(NaN==NaN)//false
console.log(""===0)//false
console.log("0"===0)//false
console.log(123==="123")//false
console.log(null===undefined)//false
console.log(NaN===NaN)//false
NaN感觉六亲不认。。。。
==两边值得类型不一样会强制转换成number再进行比较
Object.is():与===基本一致,不同(Object.is(+0,-0) false;Object.is(NaN,NaN) true)
eval是什么
它的功能是将对应的字符串解析成js并执行,应该避免使用js,因为非常消耗性能(2次,一次解析成js,一次执行)