熟悉有关JS的内容
JavaScript是一种高级的、解释型的编程语言,它支持面向对象编程。
JavaScript语言中采用的是弱类型的变量类型,对使用的数据类型未做出严格的要求。
JavaScript是一种采用事件驱动的脚本语言,它不需要经过Web服务器就可以对用户的输入做出响应。
JavaScript脚本语言不依赖于操作系统,仅需要浏览器的支持。
1 JS 数据类型
值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、null、undefined、Symbol。
引用数据类型:对象(Object)、数组(Array)、函数(Function)。
注:Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。
1.可以利用typeof 查看数据类型
<script>
let a = [1,23],str = "ghy",b = null,c = undefined,d = {name:'lx'}
function fn(){
console.log('123')
}
console.log(typeof a) // object
console.log(typeof str) // string
console.log(typeof b) // object
console.log(typeof c) // undefined
console.log(typeof d) // object
console.log(typeof fn) // function
console.log(typeof new Date()) // object
</script>
2.数据类型转换
(1)转换成字符串,可以使用Stirng(),toString()
let a =123, b = true
console.log(String(a),a.toString())
console.log(String(b),b.toString())
(2)转换成数字,可以使用Number(),parseInt(),parseFloat()
let a ='123', b = '12.34',c = new Date()
console.log(Number(a),parseInt(a),parseFloat(a)) //123,123,123
console.log(Number(b),parseInt(b),parseFloat(b)) // 12.34,12,12.34
console.log(Number(true),Number(false)) // 1,0
console.log(Number(c),c.getTime())
(3)自动转换为字符串,当尝试输出一个对象或一个变量时 JavaScript 会自动调用变量的 toString() 方法
3.==与===区别
两个等号==:如果两个值类型相同,再进行三个等号(===)的比较,如果两个值类型不同,也有可能相等,需根据以下规则进行类型转换在比较:
(1)如果一个是null,一个是undefined,那么相等
(2)如果一个是字符串,一个是数值,把字符串转换成数值之后再进行比较
== 数据类型不同,转换规则
对象==字符串,是将对象.toString()之后,转变为字符串
null == undefined 相等,但是和其他值比较就不在相等了
NaN == NaN不相等
剩下的都是转换成数字
三个等号===:如果类型不同,就一定不相等。
4.判断对象是否为空
(1)使用JSON.stringify()
let obj = {}
console.log(JSON.stringify(obj) == '{}') // true
(2)利用for…in
function isEmpty(obj){
for(let key in obj){
return true
}
return false // 如果是空对象返回false
}
let obj = {}
console.log(isEmpty(obj))
(3)利用Object.keys(), 该方法会返回一个由给定对象的自身可枚举属性组成的数组。
let obj = {}
let arr = Object.keys(obj)
if(arr.length == 0){
console.log('空')
}
2 null与undefined区别
1.null是一个表示”无”的对象,转为数值时为0;undefined是一个表示”无”的原始值,转为数值时为NaN。
2.null 和 undefined 的值相等,但类型不等。
null表示”没有对象”,即该处不应该有值。用法是:
(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。
undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。用法是:
(1)变量被声明了,但没有赋值时,就等于undefined。
(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。
(3)对象没有赋值的属性,该属性的值为undefined。
(4)函数没有返回值时,默认返回undefined。
<script>
console.log(Number(null)) // 0
console.log(Number(undefined)) // NAN
console.log(null == undefined) // true
console.log(null === undefined) // false
</script>
3 原型和原型链
4 闭包及其优缺点
5 call、applay、bind 异同
6 DOM事件流和事件委托
1.当事件发生在某个DOM节点上时,事件在DOM结构中进行一级一级的传递,这便形成了“流”,事件流便描述了从页面中接收事件的顺序。
2.DOM2级事件中规定事件流包含3个阶段:捕获阶段->处于目标阶段->冒泡阶段
addEventListener方法的第三个参数是一个布尔值(可选),指定事件处理程序是否在捕获或冒泡阶段执行。 当为true时,则事件处理程序将在捕获阶段执行。
3.事件捕获:先由不具体的节点(即上层节点)接收到事件,然后一级一级往下传递,直到最具体的目标节点接收到事件。
window->document>html->body->….
4.事件冒泡:是从最具体的目标对象开始,一层一层地向上传递,直到window对象。
使用event.stopPropagation()方法阻止事件冒泡过程。
<button id="btn">点击</button>
<script>
var btn = document.getElementById('btn')
var bodyNode = document.querySelector('body')
var htmlNode = document.querySelector('html')
btn.addEventListener('click',function(){
console.log('button click')
},true)
bodyNode.addEventListener('click',function(){
console.log('body click')
},true)
htmlNode.addEventListener('click',function(){
console.log('html click')
},true)
window.addEventListener('click',function(){
console.log('window click')
},true)
</script>
5.事件委托
简单说,事件委托就是把本来该自己接收的事件委托给自己的上级(父级,祖父级等等)的某个节点,再利用事件冒泡影响设置的子节点。事件委托可以减少内存消耗,提高性能。
假设一个ul中有多个li,点击每个li都响应一个事件。如果给每个li都绑定事件,那么对于内存消耗非常大,此时就可以给ul绑定一个方法。这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。
<ul>
<li class="lis1">lis1</li>
<li class="lis2">lis2</li>
<li class="lis3">lis3</li>
<li class="lis4">lis4</li>
</ul>
<script>
var box = document.querySelector('ul')
box.addEventListener('click',function(e){
console.log(e)
let name = e.target.className
switch (name) {
case 'lis1':
console.log('lis1 click')
break;
case 'lis2':
console.log('lis2 click')
break;
case 'lis3':
console.log('lis3 click')
break;
case 'lis4':
console.log('lis4 click')
break;
default:
break;
}
})
</script>
7 cookie、session、localStorage、sessionStorage的区别
cookie和session是可以与服务端进行通信的。localStorage 和 sessionStorage 属性允许在浏览器中存储 key/value 对的数据,不与服务器交互通信。存储大小5MB。只能存储字符串。存储的数据直接本地获取,比HTTP请求快。
cookie
保存在浏览器端,单个数据大小不超过4KB,是服务器发送到客户端的特殊信息,保存成字符串类型以文本的方式保存在客户端,会随着每次HTTP请求头发送到服务器端。如果不在浏览器中设置过期时间,cookie被保存在内存中,浏览器关闭就会删除这些cookie信息;如果设置了过期时间,cookie被保存在硬盘中,直到过期时间才会删除这些信息。
cookie应用场景:记录是否登录,上次登录时间,浏览的页面,浏览次数等。
cookie的缺点:
1)存储空间很小,不超过4kb,存储数量限制
2)用户可以操作(禁用或者修改删除)cookie,使功能受限
3)安全性较低
4)每次访问都要传送cookie给服务器,浪费带宽,如果保存过多数据影响性能。
var cookieData = document.cookie;
session
session保存在服务器端内存中,没有大小限制。服务器端创建session对象时会检测客户端请求有没有包含sessionId,如果没有就创建一个并且返回给客户端,客户端一般记在cookie里,如果HTTP请求带着sessionId,就返回对应的session对象。如果用户禁用cookie,需要使用response.encodeURL(url)进行URL重写把sessionID拼在url后面。每次启动session_start后前一次的sessionID就失效或者session过期后sessionID也会失效。
session应用场景:保存用户登录信息,防止用户非法登录等。
session的缺点:
1)Session保存的东西越多,就越占用服务器内存,对于用户在线人数较多的网站,服务器的内存压力会比较大。而且当用户离开网站后,这些session还会保存一段时间,造成资源浪费。
2)重启服务器,session数据会丢失。
3)依赖于cookie(sessionID保存在cookie),如果禁用cookie,则要使用URL重写,不安全
localStorage
localStorage 用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。
localStorage应用场景:长期登录的保存登录信息。
localStorage.setItem("key","value")
var local = localStorage.getItem("key")
sessionStorage
sessionStorage 用于临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页之后将会删除这些数据。
sessionStorage应用场景:每次打开网页需要重新登录的登录信息。
sessionStorage.setItem("key", "value");
var storage = sessionStorage.getItem("key");
参考链接
关于cookie、session、sessionStorage、localStorage各自的特点和使用方法
8 Array对象的常见方法
1.数组属性
constructor:返回创建数组对象的原型函数。
length:设置或返回数组元素的个数。
prototype:允许你向数组对象添加属性或方法。
2.Array 对象方法(常用)
concat():连接两个或更多的数组,并返回结果。
var arr =[1,3,4,6,7,89,875],trr = ['lala','yaya'], temp = [7,6,43,2]
console.log(arr.concat(trr,temp)) //将三个数组拼接
every():检测数值元素的每个元素是否都符合条件。
//检测数组是否所有元素大于5
var res = arr.every((item)=>{
return item>5
})
console.log(arr) // fasle
filter():检测数值元素,并返回符合条件所有元素的数组。
//返回数组中所有元素小于等于5的元素
var arr =[1,3,4,6,7,89,875]
var arr = arr.filter((item)=>{
return item <=5
})
console.log(arr) // [1,3,4]
//手写实现filter
Array.prototype.myFilter = function (callback) {
let newArr = []
for(let i = 0; i < this.length; i++){
if(callback(this[i])){
newArr.push(this[i])
}
}
return newArr
}
let arr = [1,2,5,7,8,90,0,4]
arr = arr.myFilter((item)=>{
return item > 4
})
console.log(arr)
find():返回符合传入测试(函数)条件的数组元素。
//返回数组中第一个大于7的元素
var arr =[1,3,4,6,7,89,875]
var res = arr.find((item)=>{
return item > 7
})
console.log(res) // 89
findIndex():返回符合传入测试(函数)条件的数组元素索引。
//返回数组中第一个大于7的元素的索引值
var arr =[1,3,4,6,7,89,875]
var res = arr.findIndex((item)=>{
return item > 7
})
console.log(res) // 5
forEach():数组每个元素都执行一次回调函数。不会修改原有数组
from():通过给定的对象中创建一个数组。
//通过字符串创建一个数组:
var arr = Array.from("tgfu788");
includes():判断一个数组是否包含一个指定的值。
//判断数组中是否存在5这个元素
var arr =[1,3,4,6,7,89,875]
var res = arr.includes(5)
console.log(res) // fasle
indexOf():搜索数组中的元素,并返回它所在的位置。
isArray():判断对象是否为数组。
join():把数组的所有元素放入一个字符串。
lastIndexOf():搜索数组中的元素,并返回它最后出现的位置。
map():通过指定函数处理数组的每个元素,并返回处理后的数组。
pop():删除数组的最后一个元素并返回删除的元素。
push():向数组的末尾添加一个或更多元素,并返回新的长度。
reduce():将数组元素计算为一个值(从左到右)。
reverse():反转数组的元素顺序。
shift():删除并返回数组的第一个元素。
slice():选取数组的一部分,并返回一个新数组。
splice(index,howmany,item1,…..,itemX):从数组中添加或删除元素。
参数 | 描述 |
---|---|
index | 必需。规定从何处添加/删除元素。该参数是开始插入和(或)删除的数组元素的下标,必须是数字。 |
howmany | 可选。规定应该删除多少元素。必须是数字,但可以是 “0”。如果未规定此参数,则删除从 index 开始到原数组结尾的所有元素。 |
item1, …,itemX | 可选。要添加到数组的新元素 |
移除数组的第三个元素,并在数组第三个位置添加新元素:
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.splice(2,1,"Lemon","Kiwi"); // Banana,Orange,Lemon,Kiwi,Mango
在数组中第三个元素位置处添加新元素:
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.splice(2,0,"Lemon","Kiwi"); // Banana,Orange,Lemon,Kiwi,Apple,Mango
//删除数组中下标为i的元素
arr.splice(i,1)
//删除数组中从下标为i元素开始的两个元素
arr.splice(i,2)
some():检测数组元素中是否有元素符合指定条件。
// 检测数组中是否还有大于7的元素
var arr =[1,3,4,6,7,89,875]
var res = arr.some((item)=>{
return item > 7
})
console.log(res) // true
sort():对数组的元素进行排序。
toString():把数组转换为字符串,并返回结果。
unshift():向数组的开头添加一个或更多元素,并返回新的长度。
valueOf():返回数组对象的原始值。
9 Date对象的常见方法
Date 对象用于处理日期与时间。
创建 Date 对象: new Date()
以下四种方法同样可以创建 Date 对象:
var d = new Date();
var d = new Date(milliseconds);
var d = new Date(dateString);
var d = new Date(year, month, day, hours, minutes, seconds, milliseconds);
1.Date 对象属性
constructor:返回对创建此对象的 Date 函数的引用。
prototype:使您有能力向对象添加属性和方法。
2.Date 对象方法
getDate():从 Date 对象返回一个月中的某一天 (1 ~ 31)。
getDay():从 Date 对象返回一周中的某一天 (0 ~ 6)。
getFullYear():从 Date 对象以四位数字返回年份。
getHours():返回 Date 对象的小时 (0 ~ 23)。
getMilliseconds():返回 Date 对象的毫秒(0 ~ 999)。
getMinutes():返回 Date 对象的分钟 (0 ~ 59)。
getMonth():从 Date 对象返回月份 (0 ~ 11)。
getSeconds():返回 Date 对象的秒数 (0 ~ 59)。
getTime():返回 1970 年 1 月 1 日至今的毫秒数。
parse():返回1970年1月1日午夜到指定日期(字符串)的毫秒数。
setTime():setTime() 方法以毫秒设置 Date 对象。
toString():把 Date 对象转换为字符串。
valueOf():返回 Date 对象的原始值。
10 String对象的常见方法
1.String 对象属性
constructor:对创建该对象的函数的引用
length:字符串的长度
prototype:允许您向对象添加属性和方法
2.String 对象方法
charAt():返回在指定位置的字符。
charCodeAt():返回在指定的位置的字符的 Unicode 编码。
concat():连接两个或更多字符串,并返回新的字符串。
indexOf():返回某个指定的字符串值在字符串中首次出现的位置。
includes():查找字符串中是否包含指定的子字符串。
lastIndexOf():从后向前搜索字符串,并从起始位置(0)开始计算返回字符串最后出现的位置。
match():查找找到一个或多个正则表达式的匹配。
replace():在字符串中查找匹配的子串, 并替换与正则表达式匹配的子串。
slice():提取字符串的片断,并在新的字符串中返回被提取的部分。
split():把字符串分割为字符串数组。
substr():从起始索引号提取字符串中指定数目的字符。
substring():提取字符串中两个指定的索引号之间的字符。
toLowerCase():把字符串转换为小写。
toUpperCase():把字符串转换为大写。
trim():去除字符串两边的空白
toLocaleLowerCase() 根据本地主机的语言环境把字符串转换为小写。
toLocaleUpperCase() 根据本地主机的语言环境把字符串转换为大写。
valueOf() 返回某个字符串对象的原始值。
toString() 返回一个字符串。
10 let、const、var 的区别
1.var声明变量存在变量提升,let和const不存在变量提升。
console.log(a); // undefined,a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 100;
console.log(c); // 报错: Uncaught ReferenceError: Cannot access 'c' before initialization
const c = 100;
2.let、const都是块级局部变量,const需要在声明变量时就赋值,并且只能进行一次赋值,即声明后不能再修改。
let a
a = 1
const b = 2
b = 3 // 报错
console.log(a)
console.log(b)
3.如果const声明的是复合类型数据,可以修改其属性。
const obj = {
name:'lx'
}
obj.name = 'xin'
console.log(obj)
4.同一作用域下let和const不能声明同名变量,而var可以。
var c = 1
var c = 2
console.log(c)
let a = 1
let a = 2 // 报错
const b = 1
const b = 2 // 报错
5.暂时性死区
在这运行流程进入作用域创建变量,到变量可以被访问之间的这一段时间,就称之为暂时死区。
ES6规定,let/const 命令会使区块形成封闭的作用域。若在声明之前使用变量,就会报错。
12 自定义实现new
13 函数防抖和节流
14 箭头函数与普通函数区别
15 手写实现Promise
16 async与await的理解
17 this的指向问题
函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。this是在运行的时候绑定的!!!!
1.在全局环境中(在任何函数体外部),this都是指向全局对象。在浏览器中,window对象即是全局对象。
console.log(this) // window
2.在普通函数环境中,this指向取决于函数调用的方式
function fn(){
'use strict'
console.log(this) // 如果不在严格模式下,this指向window,如果是严格模式下,this为undefined
}
fn()
3.箭头函数环境中,箭头函数没有自己的this,它里面的this的指向是继承函数所属上下文this
fnarrow=()=>{
console.log(this) // window
}
fnarrow()
4.当函数作为对象的方法被调用时,this指向调用的该函数的对象。这样的行为,根本不受函数定义方式或位置的影响。可以先定义这个对象,之后通过对象添加属性为这个函数也是一样的结果。
let obj = {
name: 'lx',
fun: function(){
console.log('I am '+this.name)
}
}
obj.fun()
5.对于在对象原型链上某处定义的方法,this指向的是调用这个方法的对象。
6.当函数被用作事件处理函数时,它的this指向触发事件的元素。
var btn = document.getElementById('btn')
btn.addEventListener('click',function(){
console.log(this) // <button id="btn">点击</button>
})
7.当一个函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象。
function Fun(){
this.name = 'lx'
}
var person = new Fun()
console.log(person.name)
8.可以利用call,apply,bind修改this的指向。
18 JS 的运行机制
19 JS的垃圾回收机制
在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是一种自动的内存管理机制。寻找不再使用的变量,予以释放,以让出内存,这种内存资源管理,称为垃圾回收。JS垃圾回收的方式有标记清理,引用计数。
1.标记清理:是JavaScript最常用的垃圾回收策略。当变量进入上下文,这个变量会被加上存在上下文中的标记,当变量离开上下文时,也会被加上离开上下文中的标记。给变量加上标记的方法有很多种,比如当变量进入上下文时,反转某一位;或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。垃圾回收程序运行的时候,会标记内存中存储的所有变量,然后它将所有上下文中的变量,以及被上下文中的变量引用的变量的标记去掉,在此之后再被加上标记的变量就是带删除的了,原因是任何在上下文中的变量都无法访问到它们了。
2.引用计数,对每个值都记录它被引用的次数,当一个值的引用数为0,就说明没有办法再访问到这个值了,因此可以安全地回收内存了。垃圾回收程序下次运行就会释放引用数为0的值的内存了。但是引用计数会有一个很严重的问题:循环引用,就是对象A有一个指针指向对象B,对象B也引用了对象A。
function problem() {
let objectA = new Object();
let objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
在这个例子中,objectA 和 objectB 通过各自的属性和引用,意味着它们的引用数都是2.在标记清理的策略下,这不是问题,因为在函数结束后,这两个对象都不在作用域中。而在引用计数策略下,objectA 和 objectB 还会存在,因为它们的引用计数永远不会变为0。如果函数被多次调用,则会导致大量内存不会被释放。
参考链接
20 JS 内存泄漏
不再使用的内存没有及时释放,就叫做内存泄漏。内存泄漏可能会导致应用程序卡顿或者崩溃。
哪些问题可能会导致内存泄漏?
1.意外的全局变量
function fn(){
a = 1
}
可以使用严格模式避免意外的全局变量。
2.被遗忘的定时器
//记得
clearTimeout(timer)
clearInterval(timer)
3.闭包
使用完之后可以将其赋值为null或者重新分配。
BOM / DOM对象泄漏、script中存在对BOM / DOM对象的引用、javaScript对象泄漏、闭包函数导致的泄漏。
21 实现继承的几种方式
1.利用原型链
function Parent(){
}
Parent.prototype.name = '123'
Parent.prototype.say = function(){
console.log('say hi')
}
function Child(){
}
Child.prototype = new Parent()
var child = new Child()
console.log(child.name) // 123
child.say() // say hi
2.利用构造函数+call方式
function Parent(){
this.name = '123'
}
function Child(){
Parent.call(this)
this.age = 18
}
Parent.prototype.say = function(){
console.log('say hi')
}
var child = new Child()
console.log(child.name)
child.say() // 报错
如果父类的属性都在构造函数内,就会被子类继承。如果父类的原型对象上有方法,子类不会被继承。
3.原型链与call方法组合方式
function Parent(){
this.name = '123'
}
Parent.prototype.say = function(){
console.log('say hi')
}
function Child(){
Parent.call(this)
this.age = 18
}
Child.prototype = new Parent()
var child = new Child()
console.log(child.name)
console.log(child.age)
child.say()
22 JS 作用域链
js作用域包括函数作用域、全局作用域。es6出现块级作用域。
作用域链的作用是保证执行环境里有权访问的变量和函数是有序的。如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域。作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的。
23 es6 新特性
1.let,const,块级作用域
2.箭头函数
3.模板字符串
4.解构赋值
5.模块导入(import),模块导出(export,export default)
6.类,使用extends继承,重写构造器,super关键字
7.迭代器,for of
8.Proxy
9.新增数据类型Set,Map,Symbol等
10.原有内置对象API增加
Array.from
Array.prototype.find
Array.prototype.fill
Array.prototype.findIndex
String.prototype.includes等
11.Promise
24 Object.assign()
25 script标签的defer和async属性
JS下载解析时候会阻塞DOM树的构建,放在HTML顶部 的时候会有可能出现长时间白屏的情况,想让JS解析时候阻塞DOM树构建的话必定会谈到defer和async两个属性,async,defer可以用于解决同步阻塞的情况。
浏览器在加载页面的时候,如果遇到了script标签的async属性,就会立即下载,与此同时继续加载页面。可是async下js脚本什么时候执行顺序就不确定了,有时页面还未加载完毕就执行了,有时页面加载完后才执行。因为这种不确定性,如果脚本是需要修改DOM的,就有可能会出错,因此async适合第三方脚本。
浏览器在加载页面的时候,如果遇到了script标签的defer属性,就会立即下载,与此同时继续加载页面,但是不管脚本是否下载完毕,都会等到浏览器解析完文档之后再执行脚本,因此defer比较适合与DOM有关联的脚本。
使用这两个属性需要注意兼容性问题,如果浏览器不能够识别,还是将script标签放在body下方比较好。
26 执行上下文
当我们执行一个方法时,JS会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中有这个方法的私有作用域、上层作用域指向、方法的参数、私有作用域中定义的变量以及this对象。
变量和函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。
执行上下文主要分为分为全局上下文、函数上下文。
全局上下文是最外层的上下文,在浏览器中全局上下文就是我们所说的window对象。因此所有通过var定义的变量和函数都会成为window对象的属性和方法,使用let和const的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数上下文会被推到一个上下文栈上。在函数执行完成后,上下文栈会弹出该函数上下文,将控制权还给之前的执行上下文。
27 尾调用优化
尾调用优化:ECMAScript6规范新增了一项内存管理优化机制,让JavaScript引擎在满足条件时可以重用栈帧,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。
尾调用优化的条件:
1.代码在严格模式下执行(主要是因为在非严格模式下函数调用中允许使用f.arguments和f.callee,而他们会引用外部函数的栈帧,就不能够优化了)。
2.外部函数的返回值是对尾调用函数的调用。
3.尾调用函数返回后不再需要执行额外的逻辑4.尾调用函数不是引用外部函数作用域中自由变量的闭包。
28 JS引擎
28.1 JS(V8)引擎工作机制
JS 引擎将代码转换为机器可读语言的引擎,将 JS 代码编译成机器码,还负责执行代码、分配内存以及垃圾回收。
V8 是 Google 基于 C++ 编写的开源高性能 Javascript 与 WebAssembly 引擎。用于 Google Chrome(Google 的开源浏览器) 以及 Node.js 等。
V8 实现了 ECMAScript 与 WebAssembly,能够运行在 Windows 7+、macOS 10.12+ 以及使用 x64、IA-32、ARM、MIPS 处理器的 Linux 系统,参看 ports。V8 能独立运行,也能嵌入到任何 C++ 应用当中。
解析器:将JS源代码解析成AST抽象语法树
解释器:将AST解释成bytecode,并且有用解释执行bytecode的能力
编译器:负责编译出运行更加高效的机器代码
在 V8 早期5.9版本之前,是没有解释器的,有两个编译器,编译流程是这样的:JS 由解析器解析后,生成AST抽象语法树,然后由 Full-codegen 编译器直接使用 AST 编译成机器代码,不进行任何中间转换。Full-codegen 编译器被称为基准编译器,他生成的是未被优化的机器代码,这样做的好处:第一次执行JS时就是用高效的机器代码。当代码运行一段时间后,V8 中的分析线程收集了足够的数据,来帮助另一个编译器,Crankshaft 来做代码的优化,然后需要优化的代码重新解析生成 AST,然后 Crankshaft 使用生成好的 AST 在生成优化后的机器代码,来提升运行效率。Crankshaft 又被成为优化编译器。这样设计的初衷是好的,减少了抽象语法树到字节码的转换时间,提高外部浏览器中的 JS 执行的性能。但是这样的架构设计也带来了一些问题:
1.生成的机器码占用大量的内存
2.缺少中间层的字节码,无法实现一些优化策略
3.无法很好地支持和优化JS新语法特性。
V8 引擎由很多子模块构成,其中有四个最重要:
Parser:负责将JS代码转换为 AST 抽象语法树
Ignition:解释器,负责将 AST 抽象语法树转换为ByteCode,解释执行 ByteCode,在不断运行过程中解释器收集到了很多可以用来优化代码的信息,比如变量类型,这些信息会发送给编译器。
TurboFan:编译器,利用 Ignition 收集的类型信息和字节码,编译出经过优化的机器代码。
Orinoco:垃圾回收模块,负责将程序不在需要的内存空间回收。
优化策略:
1.函数只声明未被调用,不会被解析生成 AST
2.函数只被调用一次,ByteCode直接被解释执行
3.函数被调用多次,可能会被标记为热点函数,可能会被编译成机器代码
28.2 JS引擎解析JS代码
分为两个阶段:语法检查和运行阶段。
第一阶段语法检查:分为词法分析和语法分析,词法分析就是JS解析器将JS源码按照ECMAScript标准转换词法单元。语法分析过程就是将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,这个树被称为“抽象语法树”。
第二阶段运行阶段:预解析阶段和执行阶段。
预解析阶段:
1.创建执行上下文,JS引擎将语法检查正确后的AST复制到当前执行上下文中。创建的执行上下文包括变量对象,作用域链、this(this值在进入上下文阶段确定,一旦进入执行阶段this值不会再变化)
2.属性填充,JS引擎会对语法树当中的变量对象/活动对象(AO/VO)的变量声明、函数声明、形参进行属性填充。
执行阶段:进入执行代码阶段,VO/AO就会重新赋予真实的值,“预解析”阶段赋予的undefined值会被覆盖。此阶段才是程序真正进入执行阶段,JS引擎会一行一行的读取代码。此时变量会重新赋值。
29 JS中变量提升与函数提升及其优先级
变量提升指的是使用var声明的变量提升到他所在的作用域的最顶端。
函数提升只针对具名函数,而对于赋值的匿名函数,并不会存在函数提升。
函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。而且存在同名函数与同名变量时,优先执行函数。
30 纯函数
纯函数:执行过程没有副作用,例如网络请求,输入和输出设备,DOM操作等。相同的输入得到相同的输出,它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。
优点:可缓存
// 缓存函数
function memorize(fn, resolver) {
// 缓存对象,存放参数和结果的对应关系
let cache = {};
let memorized = (...arg) => {
const key = resolver(...arg);
if (cache[key]) {
return cache[key];
} else {
cache[key] = fn(...arg);
return cache[key];
}
}
return memorized;
}
// 测试代码
function fn(a, b) {
console.log('执行');
return a + b;
}
function resolver(...arg) {
return JSON.stringify(...arg);
}
let memo = memorize(fn, resolver);
console.log(memo(1,2));
console.log(memo(1,2));
console.log(memo(1,2));
31 柯里化
什么是柯里化?是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
function curry(fn) {
// 形参长度
let length = fn.length;
let curried = (...arg) => {
// 如果传过来的数组长度小于实现需要的参数长度
if (arg.length < length) {
return (...rest) => {
return curried(...arg, ...rest);
}
}
return fn(...arg);
}
return curried;
}
// 测试代码
function add(a,b,c) {
return a+b+c;
}
let fn = curry(add);
console.log(fn(1,2,3))
console.log(fn(1)(2,3))
console.log(fn(1)(2)(3))
32 instanceof
instanceof 操作符用于检测对象是否属于某个 class,同时,检测过程中也会将继承关系考虑在内。
instanceof 可以在继承关系中用来判断一个实例是否属于它的父类型。
自定义实现 myIntanceOf:通过判断对象的原型链上是否能找到对象的 prototype,来确定 instanceof 返回值
function myIntanceOf(L, R) {
let prototype = R.prototype;
L = L.__proto__;
while (true) {
if (L === null) {
return false;
}
if (L === prototype) {
return true;
}
L = L.__proto__;
}
33 浅比较
function shallowEqual(obj1, obj2) {
if (obj1 === obj2) {
return true;
}
if (typeof obj1 != 'object' || obj1 === null || typeof obj2 != 'object' || obj2 === null) {
return false;
}
let keys1 = Object.keys(obj1);
let keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of obj1) {
if (!obj2.hasOwnProperty(key) || obj2[key] !== obj1[key]) {
return false;
}
}
return true;
}
- 本文作者: étoile
- 版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!