一.数据类型
1. JavaScript有哪些数据类型,它们的区别?
Javascript有八种数据类型:Number,String,undefined,null,Boolean,Symbol,BigInt,Object。
其中Symbol和BigInt是ES6中新增的数据类型:
- symbol代表创建后独一无二且不可变的数据类型,主要是为了解决可能出现的全局变量冲突的问题
- BigInt是一种数字类型的数据,它可以表示任意精度格式的整数,适用BigInt可以安全的存储和操作大整数,即使这个数已经超过了Number能够表示的安全整数范围。
这些数据可以分为原始数据类型和引用数据类型:
两种数据类型的区别主要是存储的位置不同:
- 原始数据类型直接存储在栈中的简单的数据段,占据的空间小,大小固定,因为频繁被使用,因此被存入栈中
- 引用数据类型存储在堆中,占据的空间大小不一,如果将这些数据存储在栈中则会影响程序的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当使用时会先在栈中找到栈中引用堆得地址然后通过地址获得堆中的实体
关于堆和栈的概念存在数据结构和操作系统内存中
数据结构中:
栈:采用先进后出的模式来存取数据
堆:优先队列,按照优先级来进行排序,优先级可以按照大小来规定
操作系统中:
栈:栈区的内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。操作方式也是先进后出
堆:堆中的内存一般由开发者分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。
注意:面试时可以分为五点,一一展开来描述,先说基本类型有什么,再说ES6新增的两个数据类型,其次就是基本数据类型和引用数据类型的区别,最后就是他们的存储方式
2. 数据类型的检测方式?
typeof:
引用类型的变量和null会被判断为Object,其余均正常
instanceOf:
可以判断对象的类型,不能用来判断基本类型数据。其原理使用的是原型链
constructor:
constructor有两个作用,一个是用来判断数据的类型,二是实例对象通过constructor对象访问它的构构造函数。注意,如果我们改变了一个函数对象的原型,那他的实例对象就不能使用constructor来判断类型。
Object.prototype.toString.call()
使用Object原型上的toString方法来判断数据类型
3. 判断数组的方式有哪些?
- Object.prototype.toString.call()
- Array.isArray(obj)方法
- instanceOf
- Array.prototype.isPrototypeOf(obj):在obj的原型链上搜索Array
4. null和undefined的区别
首先的话说一下共同点,他们都是属于JavaScript的基本数据类型,而且这两个类型的值都是唯一的,就是它们本身。
接着他们最主要的区别:
- undefined是指当前的变量没有被定义赋值,没有定义;而null是指我们手动给当前变量赋了一个null,并且他代表的往往是一个空的对象(并且往往在变量不再使用的时候将变量指控,便于浏览器回收上一个垃圾对象引用)
- typeof的返回值类型不同,undefined是返回undefined,而null是返回一个Object
关键字:未定义,空对象
5.为什么null返回一个Object
对于这个问题各方解释不一,有人说是bug,有人说就是设计如此。
我认为:
按照null的定义其实是一个空对象的话这样解释也是合理:
从逻辑角度来看,null值表示一个空对象指针,而这正是使用typeof操作符检测null值时会返回“object”的原因。
深层此理解:
在JavaScript第一个版本中,所有的值都存储在32位的单元中,每个单元包含一个小的类型标签(1-3bits)以及我们要真实存储值的真实数据。类型标签存储在每个单位的低位中。
000: object - 当前存储的数据指向一个对象。
1: int - 当前存储的数据是一个 31 位的有符号整数。
010: double - 当前存储的数据指向一个双精度的浮点数。
100: string - 当前存储的数据指向一个字符串。
110: boolean - 当前存储的数据是布尔值。
如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位。
其中有两种特殊的类型:
- undefined的值是(-2)的30次方
- null的值是机器码NULL指针(null指针的值全是0)
也就是null的类型标签和object的类型标签是一样的,因此会被判定位Object。
关键字:类型标签,null和Object的类型标签都是000.
我的理解:其实就是JavaScript最早期的时候使用32位单元来存储值,每个单元包含一个类型标签,这个类型标签就是判定数据类型的东西。然而null类型的类型标签和Object被设计成相等的。因此null是Object类型
6.intanceof 操作符的实现原理及实现
A instance B
首先先需要清楚instance是干什么的:如果B函数的显式原型对象在A对象的原型链上则返回true
面试官:小伙子,手写一个instanceof吧!
好的!
function instancof(left, right) {
let leftp = left.__proto__;
let rightp = right.prototype;
while (true) {
if (leftp == null) {
false
}
if (leftp == rightp) {
return true
}
leftp = leftp.__proto__
}
}
其实手写一个instance并不算什么难点,下面我们在来出几道题来刺激刺激你:
console.log(Object instanceof Function)
console.log(Object instanceof Object)
console.log(Function instanceof Function)
console.log(Function instanceof Object)
function Foo(params) { }
console.log(Object instanceof Foo);
是不是感到头大,其实这个结合上面的图很容易得出答案,只要我们谨记,instance的原理是判断后面的函数对象的原型对象是否在前面的示例对象的原型链上的,这样我们就可以很容易的写出原理和判断结果了。
7. 为什么0.1+0.2 ! == 0.3,如何让其相等?
关键字:精度丢失
这是浏览器在计算时的一个流程:
首先计算机在进行运算时会先将数据转为二进制的数,再去进行后续运算。然,在现在的浏览器中都是使用浮点数形式的二进制数来存储二进制数,因此还要讲将二进制数转为浮点数。最后将得到的二进制浮点数进行相加。计算结束后将浮点数转为十进制数。
注意:浮点数分为32位操作系统的单精度浮点数和64位操作系统的双精度浮点数。
对于这个问题的解答:
计算时发生了两次精度丢失。
第一次精度丢失:将二进制的0.1和0.2转为浮点数时,由于二进制的浮点数的小数位只能存储52位,导致小数点后53位的数要进行为1则进为0则舍的操作,这里会造成精度丢失。
第二次精度丢失:二进制的浮点数进行相加时,小数位相加导致导致小数位多出一位,又要让53位的数进行为1则进为0则舍的操作
可能引起的BUG:
价格300.01,优惠300元出现支付金额不足0.01元(人民币最小面值为0.01元)等
解决方案:
可以使用Math.toFixed() 或者ES6中的Number.EPSION
8.如何获得安全的undefined值
undefined是一个z标识符,因此可以被拿到当作变量的值,但是这样做会影响undefined的正常判断。表达式void没有返回值,因此结果返回undefined。因此我们可以使用void 0 来获得安全的undefined值。
9.typeof NaN的结果是多少
首先NaN是指“不是一个数字的值”,通常在我们进行计算时发生错误会返回这个值。
typeof NaN 的值时Number
NaN时一个特殊的值,它是一个唯一不等于自身的值,也就是说NaN===NaN 的结果时false
10.isNaN与Number.isNaN函数的区别
isNaN():这个函数首先会将传入的数据尝试转成数字,如果不能转,就直接返回true,以至于String、Boolean类型的数据都会返回true
Number.isNaN(): 这种方式是es6新引入的,首先会判断内部是不是数字,不会进行类型转换。因此更加有针对性,只在数字与NaN之间比较,如果是其他直接返回false
11.==操作符的强制类型转换规则
对于==来说,两边的类型不一样就会进行类型转换,具体流程如下:
- 如果双方数据类型相同,就直接比较内容
- 如果是在比较null和undefined就直接返回true
- 如果是Number和String的话,将String转换为Number
- 如果是Boolean和Number的话,将Boolean转为Number
- 如果一方有Object的话且一方为string、number或者symbol的话,将Object转为基本类型
12.其他值转到string规则
- Null和Undefined直接转换
- Boolean直接转换
- Number类型直接转换,极大极小的数字会转成指数的形式
- symbol类型的值直接转换,但是只允许显示强制类型转换,隐式类型转换会报错
- 普通对象如果没有自己的toString()方法,就会调用Object的toString()方法,就会返回"[object Object]"
13.其他值转到数字值规则
- undefined类型转为NaN
- null类型转为0
- Boolean类型分别为0,1
- String类型如果包含数字会转为数字,如果是非数字会转为NaN,如果是空会转为0
- Symbol类型的值不能转换为数字,会报错
- 对象类型的值会先转为基本类型的值,然后根据以上规则进行转换
14.其他值转到布尔值规则
一下这些是假值:
除此之外都是真值
15.||和&&操作符的返回值
||和&&首先会对第一个操作数执行条件进行判断
- ||时第一个值是true则返回true,如果为false就返回第二个操作数
- &&时第一个值是true就返回第二个操作数,如果为false就返回false
他们返回的都是其中一个操作数的结果,而非条件判断的结果
16.Object.is()与比较操作符“=”、“”的区别
- “==”这里在进行相等判断时,如果两边的类型不一致,则会进行隐式类型转换后再去进行比较
- "==="这里比较时如果两边的类型不一致时,直接返回false
- Object.is()大体上和三等号相似,就是做了一些处理,比如NaN和NaN相等,“+0”和“-0”不想等
console.log(Object.is(NaN,NaN))//true
17.什么是JavaScript中的包装类型
在JavaScript中,基本类型是没有属性和方法的,但是为了更加方便的操作基本类型的值,在调用基本类型的属性和方法时Javascript会在内部隐式的将基本类型的值转换为对象
比如:
let a='12';
console.log(a.length)
在我们访问一些属性时,Javascript就在后台将’12’这个字符串转化为String('abc') ,然后再去访问对应的属性。
Javascript还可以通过Object(内容) 显式的将基本类型转换为包装类型,然后再去通过valueOf方法将包装类型转为基本类型。因此也常常被用于显式类型转化
18. JavaScript中如何进行隐式转换
在js中,当运算符两边数据类型不统一就无法进行运算,这时js引擎就会隐式的将运算符两边的数据类型转为相同类型再去运算。
主要涉及的三种转换方式为:
- 将引用类型值转换为原始值:ToPrimitive()
- 将值转为数值,ToNumber()
- 将值转为字符串,ToString()
主要介绍一下ToPrimitive()函数的执行过程: JavaScript基本每个值都会隐含这个参数,用来将值转换为基本类型的数据,所以经常被用于引用类型中。它的大概过程是这样的。
ToPrimitive(obj,type)
接受两个参数,obj是对象本身,type是我们期望的结果类型,一般有String和Number。
如果type为number时规则如下:
- 调用obj的valueOf方法,如果返回一个原始值,则返回,否则进入下一步
- 调用obj的toString方法,后续同上
- 抛出TypeError异常
如果type为string时规则如下:
- 调用obj的toString方法,如果返回一个原始值,则返回,否则进入下一步
- 调用obj的valueOf方法,后续同上
- 抛出TypeError异常
主要区别就在于type参数,一般我们默认的参数值时number,但是如果时Date对象的值时用的是string。
以下是基本类型的值在不同操作符下隐式转换的规则:
+操作符 :**(string与number)**两边至少有一个string类型的变量时,两边的变量都会被隐式转换为字符串,其他情况两边变量会被转为数字。
1 + '23'
1 + false
1 + Symbol()
'1' + false
false + true
-、*、/、%操作符 :**(number)**只能对于number类型进行运算,其他类型会转为number
1 * '23'
1 * false
1 / 'aa'
==操作符 :(number)
3 == true
'0' == false
'0' == 0
- 对于
< 和> 比较符
如果两边都是字符串,则比较字母的顺序
'ca' < 'bd'
'a' < 'b'
其他情况下转换为数字再去比较,包括对象,会被转换为基本类型
'12' < 13
false > -1
比如下面这行代码的输出及其内部执行:
var a = {}
a > 2
a >'a'
首先将a转为基本类型,通过valueOf()得出结果返回{},发现不是基本类型,再去执行toString()方法,返回[object Object] ,那么第一行代码输出false,第二行代码分别对第一个字符串进行对比也返回false
19.+ 操作符什么时候用于字符串的拼接
对于+操作符而言,只要某一方的数据是字符串或者可以通过某种方式转换为字符串,就进行字符串的拼接操作,比如引用类型数据被toPrimitive()函数操作。否则进行数学运算
也有例外,比如:
{}+2 //输出2
2+{} //输出"2[object Object]"
这是因为当{}在前时浏览器自动将其认为成了一个块级作用域,因此直接进行了后面的操作。不同的浏览器结果不同。所以第二个才是我想要的答案。
20.为什么会有BigInt提案
JavaScript如果在计算时数字超出了规定的最大安全数,就会出现精度丢失的情况,因此引入了Bigint的提案。
二.JavaScript基础
1.JS访问内部数据变量的方式与哪些以及使用场景?
读取和设置对象属性的方式主要有两种:
(.) 方法: obj.attr ([])方法: obj[attr]
.语法是我们平时经常使用的方式,
[]的方式通常再以下场景下使用:
- 属性名包含特殊字符:- 空格等
- 变量名不确定([]内可以直接加一个变量,或者一个字符串)
使用方式:
let p={}
let address='address'
p.name='name'
p['age']=12
p[address]='安静'
console.log(p)
2. Javascript函数调用的方式有哪四种?
- 直接调用函数(通过window)
- 通过对象调用
- new 调用(使用构造函数调用函数)
- 通过bind/call/apply调用
3.new操作符的实现原理
要想知道new操作符的原理,就得先了解new的作用是什么,我们都知道,一般将我们的构造函数实例化为对象时就要使用new操作符。
要想真正掌握并手写一个new,你需要掌握以下知识点:原型与原型链以及this绑定相关知识。
其实new的内部流程十分简单,大概就是这么几个步骤:
- 创建一个新的对象obj
- 将对象的隐式原型指向构造函数的原型对象
- 通过apply调用构造函数(改变this指向到obj)
- 返回obj,如果构造函数有返回值还需判断,如果返回值为引用类型则返回引用类型对象,如果为原始类型就返回obj
手写:
function MyNew() {
let [ArgFun, ...arg] = [...arguments]
let obj = {};
obj.__proto__ = ArgFun.prototype;
let tartgetObj = ArgFun.apply(obj, arg)
return tartgetObj instanceof Object == true ? tartgetObj : obj
}
使用:
function Person(name, age) {
this.name = name;
this.age = age;
console.log('执行')
return '12'
}
let zhangsan = MyNew(Person, '张三', 12)
console.log(zhangsan)
4.数组有哪些原生的方法
- pop()和push()尾部操作,删除和增加
- shift()和unshift()首部操作,删除和增加
- sort()排序,里面跟一个函数,带两个参数,返回值是两参数相减,正序详见为升序,反序相减为降序,返回值为数组,会改变数组
- reserve()反转数组,也返回数组,会改变数组
- concat(),拼接两个数组,返回拼接后的数组,不改变原数组
- slice(),截取数组的一部分并返回,不影响原数组
- splice(),可以删除或者替换或者插入数组中的某一个或多个元素,返回数组,改变原数组
- filter()函数,过滤数组,返回过滤后的数组,不改变原数组
- map()函数,遍历数组,不改变原数组
- reduce(),汇总,可以操作每一个数组元素,参数为一个函数和一个默认值,函数有两个参数,分别代表上一次计算的结果个本次的值,默认值就是第一次循环函数第一个参数的值。
5.什么是DOM和BOM
回答这个问题可以将ECMAScript也纳入。
JavaScript是由三部分构成的,分别是ECMAScript、DOM和BOM。它们的功能和所负责的任务也是不一样的:
ECMAScript:规定了语言的基础,比如语法、关键字、操作符等等,规定了语言的标准,比如ES5,ES6等
DOM:文档对象模型,提供了访问和操作网页内容的方法和接口。DOM把整个页面映射成立一个多层节点的结构,每个节点被视为一个对象,最终形成一个DOM树。我们要想获取某个节点的对象,只需要一层一层的拿到对象再去操作就可以。
BOM:浏览器对象模型,提供了与浏览器之间的的方法和接口。BOM的核心是window对象。window下面还提供了一些操作浏览器的api,比如history属性,navigator和location对象等。
注:window有一个双重身份,它既是js操作浏览器窗口的一个接口,也是全局的global对象,可以操作网页中任何一个变量对象等
6.对类数组(伪数组)的的理解,如何转换为数组
类数组就是伪数组,是一种类似数组的对象,拥有length属性和若干索引属性的对象,但是不可以调用数组的方法
常见的类数组对象有argument和DOM方法返回的结果。
我们常见的将类数组转换为数组的方法有这些:
1.通过call调用数组的slice方法:
Array.prototype.slice.call(arrayLike);
2.通过call调用数组的splice方法:
Array.prototype.splice.call(arrayLike, 0);
3.通过call调用数组等concat方法
Array.prototype.splice.call(arrayLike, 0)
4.通过 Array.from 方法来实现转换
Array.from(arrayLike);
5.也可以使用es6扩展运算符
7.为什么要使用尾调用?
首先尾调用简单来说就是在一个函数执行结束时,调用另一个函数。我们知道函数的执行是基于执行栈的,所以我们当前函数的最后一步出栈,然后再去调用另外一个函数。再将这个函数入栈,这样做的好处是我们不用保留当前函数的执行栈,从而节省了内存,这就是尾调用优化
8.for…in和for…of的区别
- for in 获取到的是对象的键名,for of获取到的是对象的键值
- for in会遍历对象的原型链,性能较差不推荐使用,而for of只遍历当前对象不会遍历原型链
- 对于数组的遍历,for in会返回数组中所有可枚举的属性,for of会返回数组线标的对应值。
总结:for in主要是用于遍历对象,不适用于遍历数组,for of 循环可以遍历对象、数组、类数组对象等。
9.对Ajax的理解,实现一个Ajax请求
指的是JavaScript的异步通信,异步请求数据的技术,从服务器获取到json数据,再将其更新到整个页面,并且它不会刷新整个页面。这也是Ajax最大的优点。
Ajax的核心是XmlHttpRequest对象,JavaScript可以使用XmlHttpRequest对象发出请求并且处理响应而不阻塞用户。
**工作原理:**我认为Ajax就是浏览器与服务器 之间的一个中间层,客户端发送请求,请求交给xhr对象,xhr把请求交给服务器,服务器进行业务处理,服务器又将响应的数据交给xhr,再有JavaScript写入页面。
创建Ajax请求的步骤:
- 创建XmlHttpRequest对象
- 创建一个Http请求(可以设置请求方式,请求地址,是否异步)
- 添加一些信息和监听函数(比如添加请求头信息和监听xhr的状态)
- 发送请求(post时可传入参数)
我们下面使用回调函数的方式来封装一个网络请求的函数:
function getData(url, hander) {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if (this.readyState !== 4) return
if (this.status == 200) {
hander(this.response)
} else {
console.error(this.statusText)
}
}
xhr.onerror = function () {
console.log(this.statusText)
}
xhr.setRequestHeader("Accept", "application/json")
xhr.send();
}
使用方式:
getData('http://httpbin.org/get', (data) => {
console.log(data)
})
还可以使用promise封装:
function UseP(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if (this.readyState != 4) return
if (this.status == 200) {
resolve(this.response)
} else {
reject(this.statusText)
}
}
xhr.onerror = function () {
reject(this.statusText)
}
xhr.responseType = 'json'
xhr.setRequestHeader('Accept', 'application/json')
xhr.send()
})
}
使用:
UseP('http://httpbin.org/get').then((res) => {
console.log(res)
}).catch((err) => {
console.log(err)
})
10.Ajax、axios和fetch的区别
Ajax:
是一种无需重新加载网页的的情况下,能够更新部分网页的技术。通过后台与服务器进行少量的数据交换。Ajax可以实现网页的异步过呢更新操作。传统的网页如果需要更新内容需要重新加载整个网页。
Ajax有以下缺点:
- 本身是针对MVC编程,不符合前端MVVM的浪潮
- 基于原生XHR开发,而XHR本身的架构不清晰
- 不符合关注分离的原则
- 配置和调用比较混乱,而且基于事件的异步模型不友好
axios:
用于浏览器和nodejs的HTTP客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本。有以下特则:
- 从浏览器端发起XMLHttpRequest请求
- node端发起http请求
- 支持Promise API
- 客户端支持防止CSRF
- 提供了一些并发请求的接口
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转为JSON格式
PS:防止CSRF:就是让你的每个请求都带一个从cookie中拿到的key, 根据浏览器同源策略,假冒的网站是拿不到你cookie中得key的,这样,后台就可以轻松辨别出这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略。
fetch:
Ajax的替代品。基于Promise设计。Fetch的代码结构比起ajax简单多了,参数有点像jQuery ajax。但是,一定记住fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
fetch的优点:
- 语法简洁,更加语义化
- 基于标准的Promise实现,支持async/await
- 更加底层,提供的API丰富
- 脱离了XHR
缺点:
- 只对网络请求报错,对服务器返回的状态码不报错
- fetch默认不会带cookie,需要添加配置项:fetch(url, {credentials: ‘include’})
- fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
- fetch没有办法原生监测请求的进度,而XHR可以
11.Javascript脚本延迟加载的方法有哪些?
延迟加载就是等页面加载完成之后再去加载JavaScript 文件,js延迟加载有助于提高页面加载的速度
一般有以下几种方式:
- defer属性:给js脚本添加defer属性,这个属性会让脚本的加载和文档的解析同步执行,完后再文档解析完成之后再去执行这个脚本文件,这样的话可以使我们的页面的渲染不被阻塞。多个设置了defer属性的脚本按照规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样(关键字:同步,顺序执行)
- async属性:给js脚本添加async属性,这个属性会让脚本的加载和文档的解析异步执行,不会阻塞页面的解析过程,但是当脚本加载完成之后立即执行js脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个async属性的脚本的顺序是不可预测的,一般不会按照代码的顺序依次执行。(关键字:异步,可能会阻塞,乱序)
- 动态创建DOM的方式:动态创建DOM标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建script标签来引入js脚本
- 使用setTimeout延迟:设置一个定时器来延迟加载JS脚本文件
- 将JS文件最后加载:将js脚本放在文档底部,使js脚本尽可能的再最后来加载执行
12.CommonJS和ES6模块的异同点?
区别:
- CommonJS是对模块的浅拷贝,ES6module是对模块的引用;即ES6不能改变值,指针的指向不能改变,相当于const
- import接口时read-only(只读状态),不能修改其变量值。即不能修改其变量的地址指向,但是可以改变变量内部的值。可以对CommonJS重新赋值(改变指针指向)
共同点:
CommonJS和ES6 Module都可以对引入的对象进行赋值,即对对象内部属性的值进行改变
三.ES6
1.let,var ,const的区别
回答这个问题要从一下几个方向去回答:
1.块级作用域:let和const具有块级作用域,var不存在块级作用域的概念。块级作用域解决了ES5的两个问题:内层变量可能覆盖外层变量和循环时泄露循环变量为全局变量
2.变量提升:var 存在变量提升,let和const不存在变量提升(只能在声明后使用),涉及到上下文的概念
3.给全局添加属性:在全局window下,var声明的变量是全局变量,并将变量添加到全局对象的属性上,而let和const不会。
4.重复声明:var可以重复声明,而let,const不行
5.暂时性死区:在没有声明之前,let和const的变量都是不可用的,当然这作用于块级作用域内
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
6.初始值设置:let和var可以不指定初始值,但是const必须指定初始值
7.指针指向:let创建的变量可以改变指针指向,但是const声明的变量不允许改变指针的指向。
2.const对象的属性可以修改吗?
const能保证的并不是变量的值不能改动,而是变量指向的内存地址不能改动。那么对于基本类型的数据来说,其值保存在变量指向的内存地址中,所以不能改变。相当于常量;
然而当我们变量是指向引用类型的地址时,此时我们的const只能保证它指向的地址指针不能改动,但是对于它内存中的内容我们就无法控制了。
3.如果new一个箭头函数会怎样?
首先说一下new操作符的实现步骤:
- 创建一个新的对象
- 然后将构造函数的作用域赋给新对象(也就是将对象的
__proto__ 属性指向构造函数的prototype属性) - 构造函数中的this指向该对象并且执行
- 返回新的对象
回看这个问题,箭头函数时ES6提出的,它本身没有prototype属性,也没有自己的this指向,也没有argument参数。因此它不具备new一个实例对象的条件**,因此如果直接new一个箭头函数的话会报错。报箭头函数不是一个构造函数的错误**
4.箭头函数和普通函数的区别
1.箭头函数比普通函数更加简洁
2.箭头函数没有自己的this
箭头函数的this总是指向它当前作用域的上一层
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
b: () => {
console.log(this.id);
}
};
obj.b();
3.箭头函数继承来的this指向永远不会改变
4.call()、apply()、bind()等方法不能改变箭头函数中的this指向
5.箭头函数不能作为构造函数使用
6.箭头函数没有自己的arguments
7.箭头函数没有prototype属性
8.箭头函数不能用作Generator函数,不能使用yeild关键字
5.箭头函数的this指向哪里?
箭头函数实际上并没有属于自己的this,它的this实际上时继承了所处作用域的上级的this作为自己的this值。这个this在定义时就已经确定下来,之后也不会改变。
可以使用babel将es6代码转为es5代码理解一下this:
ES6:
const obj = {
getArrow() {
return () => {
console.log(this === obj);
};
}
}
Babel编译后:
var obj = {
getArrow: function getArrow() {
var _this = this;
return function () {
console.log(_this === obj);
};
}
};
6.扩展运算符的作用及适用场景?
1.对象的扩展运算符
对象的扩展运算符用于取出对象所有可遍历的属性,拷贝到当前的对象中。
let a = {name: 'a',age: 12}
let b = { ...a }
其实扩展运算符等价于Object.assign()
let a = {name: 'a',age: 12}
let b=Object.assign({},a)
Object.assign() 方法用于方法的合并,将源对象的所有可枚举枚举的属性复制到目标对象中。
它的第一个参数时目标对象,后面的参数都是源对象
一个对象扩展运算符的适用场景,在redux的reducer中要求reducer函数必须是一个纯函数,即我们不能直接在函数内部操作state状态,所以我们可以适用扩展运算符对其直接进行拷贝产生一个新的对象进行返回
注意:扩展运算符和对象实例的拷贝都是浅拷贝
2.数组扩展运算符
数组扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。
应用场景:
- 将数组转换为参数序列
- 复制数组
- 合并数组
- 与结构赋值结合起来用于产生新的数组(注:使用扩展运算符用于数组赋值,其只能放在最后一位,否则报错)
- 将字符串转为数组**(算法题可用)**
- 将参数列表argument转化为数组(代替了
Array.prototype.slice.call(arguments) )
7.对象与数组的结构的理解?
其实就是有针对性的在数组和对象中拿取数据,其中数组时按照位置取值,对象时按照属性名名称取值,与位置无关。
在对象的解构中有时候会碰到高度嵌套的对象,这时我们如果想要提取属性时就又有一种特别的写法:{解构出来的对象:{下一层对象:{目标属性}}}
例如:
const school = {
classes: {
stu: {
name: 'Bob',
age: 24,
}
}
}
如果我们想要取到name:
let {classes:{stu:{name}}}=school
8.对rest参数的理解?
将函数的形参整个成一个数组,经常用于处理不确定函数参数个数的情况。
function mutiple(...args) {
console.log(args)
}
mutiple(1, 2, 3, 4)
10.object.assign和es6的扩展运算符是深拷贝还是浅拷贝,两者有何区别
两者都是浅拷贝。
共同点:
共同点是都是取复制旧的对象进而去创建该对象的一个副本,且都是浅拷贝
不同点:
Object.assign 函数修改其第一个传入的tartget对象,因此会触发es6的setter,修改set后如果我们使用了Proxy或者Object.defineProperty在set方法上进行其他操作,那我们就会碰到意想不到的错误- 而扩展运算符给我们一个普通的JavaScript对象,对我们后续操作并不会有什么影响
四. 原型与原型链
1.原型对象上的constructor指向哪里
每个函数都有一个prototype属性,这是一个引用,指向了函数的原型对象,每个原型对象都有一个constructor属性,这个属性指向函数对象本身。
验证一下:
function Fun(params) {}
console.log(Fun.prototype.constructor==Fun)
var a=new Fun();
console.log(a.__proto__.constructor===Fun)
2显式原型与隐式原型
首先我们需要清楚原型对象的作用:定义所有对象实例所共享的属性和方法。
-
每个函数function都有一个一个prototype ,即显式属性(属性),它只是一个引用,指向了原型对象 -
每个实例对象都有一个__proto__ ,称为隐式原型,它也是一个引用。最终也指向原型对象
实例对象上的隐式原型的值是和实例对应的构造函数prototype的是值相同的,也就是说,这两个不同的引用变量属性指向同一块地址。都指向原型对象
也就是说再下面的栗子中:控制台输出的结果是true
function Fun(params) {
}
var a=new Fun();
console.log(a.__proto__===Fun.prototype)
我们可以理解为
- 在new的时候执行了这条语句:
this.__proto__=Fun.prototype
它在内存中是这样的:
3.谈一谈原型与原型链
以下的所有概念都是自己理解的,如果有什么错误请指出。
原型:在我们JavaScript中的每一个函数,都有一个prototype属性,这个属性保存了一个引用,指向内存中的一个空的Object对象,这个对象其实就是我们这个函数的原型对象,它的作用一般就是用来共享属性和变量。怎么样共享?那肯定就是要使用到实例对象的概念,我们实例化这个函数,此时这个函数也叫做构造方法,我们实例化多个实例对象,那么我们这些实例就都可以访问到我们原型对象上的属性或者变量。
首先要知道两个概念:隐式原型(prototype )和显式原型(__proto__ )。隐式原型是实例对象具有的属性,而显式原型是函数具才有的。但是相同的是它们都是一个引用,并别都指向了同一块内存,就是我们的原型对象。
**原型链:**原型链为什么要叫做原型链?只是因为它的执行是一个链式的调用顺序。总结为一句话实际上就是沿着隐式原型寻找属性或者方法,所以也叫隐式原型链。当我们使用实例方法来执行一个属性或者方法时,现在假设我们想要的属性和方法都在原型对象中,则JavaScript内存中的执行顺序是,先通过当前实例的__proto__ 引用进行到构造函数的原型对象中,然后再此对象中寻找该属性或者方法,如果此方法中没有我们要找的属性或方法,就继续在当前原型对象中的__proto__ 属性中进入到__prote__ 所引用的内存中,也就是Object函数的原型对象(因为我们前面说到了,每个函数的prototype属性其实都是指向了一个空的Object实例,因此这个实例中肯定就有了指向Object原型对象的引用__proto__ )因此我们再继续在Object的原型对象中寻找我们需要的属性或方法。如果这里依然没有,那么就进行一些错误的处理。因为Object.prototy所指向的原型对象中的__proto__ 值为空。
接下来我们可以写一些代码来分析一下它在内存中的状态:
function Fn() {
this.test1=function(){
console.log('test()')
}
Fn.prototype.test2=function(){
console.log('test2()')
}
}
let fn=new Fn();
fn.test1();
fn.test2();
console.log(fn.toString())
console.log(fn.test3)
再内存中的图大概就是这样的
fn.test1() ,实例对象中有这个方法,所以直接直接执行fn.test2() ,首先再示例对象中查找,发现没有,然后在通过__proto__ 在原型对象中查找,发现有这个属性。执行fn.toString() ,按照上一个步骤,找到原型对象发现并没有这个属性,则在__proto__ 指向的原型对象中查找,发现,执行fn.test3 ,找到Object的原型对象后,发现依然没有,则进入Object的隐式原型,这里就已经到了尽头,因此为undefined
看了上面这么多,其实我们还可以总结出两个注意事项:
- **Object的原型对象不是Object的示例对象:**关于函数的显示原型默认是指向一个一个Object的实例对象。但是有一个特殊的地方就是Object自身的Prototype属性指向的实例对象,因为它的隐式原型属性是指向null的。
- 原型链的尽头就是Object的原型对象,因为它的隐式原型为null
4.Function,Object与原型的关系
Function是一个相对特殊的函数,因为所有的函数都是Function的示例,所以每一个函数的隐式原型都指向Function函数的原型对象。
下面这张图是一个非常经典的图。从张图中我们可以看出以下这些点:
- 所有函数都有两个属性,显式原型(指向Object的示例对象)和隐式原型(Function的原型对象)
- Function函数实际是实例化自身的产物,因此它的隐式原型执行,因此才会有它自身的隐式原型和显式原型相等(这是一个特殊的情况)
- Object函数是Function的示例,因此有了图中的Object.
__Proto__ 指向了Function.protype的示例,实际上这个结果我们也可以看出来,因为所有的函数的都是Function的示例,Object也不例外
5.原型链指向题目
既然你已经看到这里了不如做几道题吧:
p.__proto__
Person.prototype.__proto__
p.__proto__.__proto__
p.__proto__.constructor.prototype.__proto__
Person.prototype.constructor.prototype.__proto__
p.__proto__.constructor
Person.prototype.constructor
6.探索instanceOf(手写instanceof)
A instance B
首先先需要清楚instance是干什么的:如果B函数的显式原型对象在A对象的原型链上则返回true
面试官:小伙子,手写一个instanceof吧!
好的!
function instancof(left, right) {
let leftp = left.__proto__;
let rightp = right.prototype;
while (true) {
if (leftp == null) {
false
}
if (leftp == rightp) {
return
true
}
leftp = leftp.__proto__
}
}
其实手写一个instance并不算什么难点,下面我们在来出几道题来刺激刺激你:
console.log(Object instanceof Function)
console.log(Object instanceof Object)
console.log(Function instanceof Function)
console.log(Function instanceof Object)
function Foo(params) { }
console.log(Object instanceof Foo);
是不是感到头大,其实这个结合上面的图很容易得出答案,只要我们谨记,instance的原理是判断后面的函数对象的原型对象是否在前面的示例对象的原型链上的,这样我们就可以很容易的写出原理和判断结果了。
7.两道面试题
第一道:
function A() {}
A.prototype.n=1;
var b=new A();
A.prototype={
n:2,
m:3
}
var c=new A();
console.log(b.n,b.m,c.n,c.m)
分析一下:大概意思是先给A的原型对象上放了一个n属性,然后实例化对象,那么顾名思义现在这个实例化的对象的隐式原型是指向这个原型对象的;下一步A给它的函数对象重新开辟了一个原型对象,再赋两个属性并且实例化,此时的这个实例化指向的是这个原型对象,因此我们这里的两个示例对象是指向两个不同的原型对象
因此输出结果是:
1 undefined 2 3
第二道:
function F() { }
Object.prototype.a = function () {
console.log('a')
}
Function.prototype.b = function () {
console.log('b')
}
var f = new F();
f.a();
f.b();
F.a();
F.b();
分析:首先第一个是给Object的原型对象上加了一个方法,其实这就和toString()哪一类方法是同类的;第二个给Function的原型对象上加了一个方法
f.a() :f实例对象在它的原型链上找对应方法,一直找到了Object中,因此正常执行 f.b() : f实例对象直到找至原型链的重点Object.prototype.__proto__ 也没有找到,因此报错 F.a() : 通过F实例对象的隐式原型链找,顺序依次是Function.prototype.__proto__->Object.prototype.a() F.b() : 每一个函数都是Function函数的实例对象,所以F作为实例对象有自己的原型链,它的隐式原型指向Function的显示原型所指的原型对象中
五. 执行上下文/作用域链/闭包
执行上下文是用来预处理JS代码的
执行上下文栈是用来管理执行上下文的
1.执行上下文
JS代码执行时它的代码位置分为一个全局代码和函数内的代码,因此执行上下文分为全局执行上下文和函数执行上下文,
全局执行上下文
在执行全局代码前会创建一个全局执行上下文环境将window将其确定为全局执行上下文;并且对全局数据进行一个预处理,这里面就包括:
- var 定义的全局变量设为undefined,并且将其添加为window的属性
- 声明function函数,添加为window的方法
- this赋值为window
等全局执行上下文执行完毕后,才会真正开始执行代码
函数执行上下文
函数执行上下文就是函数执行前,也会创建类似于全局执行上下文的的环境,它的作用功能是对局部的数据进行预处理
- 形参变量赋值为实参,添加为执行上下文的属性
- argument赋值为实参列表,添加为执行上下文的属性
- var 定义的局部变量设为undefined,添加为执行上下文的属性
- function声明的函数赋值,添加为执行上下文的属性
- this赋值为调用函数的对象或window
函数上下文指向完毕以后,开始执行我们函数体里的代码
我们总结一下就是不管是全局执行上下文还是函数执行上下文,在他们的JS代码执行前,都会有一个执行上下文环境,来给我们代码的里面的变量,函数,this等进行声明赋值,等这个操作结束以后,才真正开始执行代码
2.执行上下文栈
在全局代码执行前,JS引擎就会创建一个栈来管理所有的执行上下文对象
- 全局执行上下文确定后,就会将其添加到栈中
- 某个函数执行上下文确定后,将其添加到栈中
- 函数执行上下文执行完毕后,出栈
- 此时栈中只剩下window
因此我们执行上下文对象的个数永远都是n+1,n是函数执行上下文的个数,1是全局执行上下文window
一道面试题:
问题:执行结果以及有几个执行上下文
console.log('g begin'+i);
var i=1;
foo(i);
function foo(i) {
if (i==4) {
return
}
console.log('foo begin:'+i)
foo(i+1);
console.log('foo end:'+i)
}
console.log('g end:'+i)
分析:首先这是一个递归函数。执行结果是:
g beginundefined
foo begin:1
foo begin:2
foo begin:3
foo end:3
foo end:2
foo end:1
g end:1
函数共执行四次,并且加上一个全局执行上下文,因此一共有5个执行上下文对象
3.作用域
对于我的理解,我认为作用域就是一段代码可执行的区域,作用域是静态的,在我们编写代码时就已经决定了它的作用域。作用域的一个最大的功能就是隔离变量, 防止变名命名冲突。
作用域分为三种,全局作用域,局部作用域以及块作用域
**全局作用域:**全局作用域就是最外层的的代码执行范围,所有未定义直接赋值的变量自动声明为全局作用域;所有window对象的属性属于全局作用于范围;全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,引起变量冲突
**函数作用域:**函数作用域时声明在函数内部的变量,一般只有固定的代码片段可以访问到,并且函数作用域时分层的,内层作用域可以访问外层做作用域,而外层作用域不能访问到内层作用域
**块级作用域:**这是ES6独有的特性,使用新增的let额const指令可以声明块级作用域,块级作用域可以在一个函数中创建,也可以在一个作用域块中创建。let和const的声明的变量不会有便量提升,也不可以重复声明。
4.作用域链
作用域链实际上就是当我们在当前作用域寻找一个变量,但是这个作用域没有这个变量,那么这个变量就是自由变量,如果在自己的作用域找不到该变量, 就依次向上级作用域查找,直到访问到window就终止,这一层层的关系就叫做作用域链。
作用域链的作用就是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到环境的变量和函数。
作用域链本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象始终是作用域链的最后一个对象
5.闭包
**闭包如何产生:**当一个嵌套的内部子函数引用了嵌套的外部函数的变量时,就产生了闭包。
**闭包到底是是什么:**闭包是嵌套的内部函数?我认为闭包实际上是一个包含我们引用外部函数变量的一个对象(通过浏览器调试得出此结论)
闭包产生的条件:
- 函数嵌套
- 内部函数引用外部函数的数据
- 执行外部函数(外部函数不执行, 内部函数不定义,函数定义就可产生闭包)
常见的闭包:
- 将函数作为另一个函数的返回值
function fn1() {
var a=2;
function fn2() {
a++
console.log(a)
}
return fn2
}
var f=fn1();
f();
f();
var f2=fn1();
f2();
f2();
fn1()()
fn1()()
- 将函数作为实参传递给另外一个函数
function shwDelay(msg,time) {
setTimeout(() => {
console.log(msg)
}, time);
}
shwDelay('123',1000)
闭包的作用:
- 使用函数内部的变量(局部变量)执行完后,仍然存活在内存中(延长了局部变量的声明周期)
- 让函数外部可以操作(读写)到函数内部的数据
闭包的生命周期:
产生:在嵌套内部函数定义执行完时就产生了,也就是在函数执行上文时(变量提升,函数提升)
死亡:在嵌套的内部函数成为垃圾对象时(对此函数的引用指向null时)
闭包的缺陷:
-
如果执行闭包函数而且进行引用指向时,如果不手动释放,就会一直存在于内存中,造成内存泄露 -
函数执行完后,函数的局部变量没有释放,占用的内存时间就会变长 function fnn() {
var arr=new Array(10000)
function fnn2() {
console.log(arr.length)
}
return fnn
}
var fnn=fnn();
fnn()
fnn=null
上面代码就是在我们闭包中使用了一个占用很大空间的数组,如果我们不在使用它了,那么它会一直停留在内存中,因此我们需要手动将其指向null。使内部函数称为垃圾对象,从而回收我们的局部变量
一道闭包经典面试题:
function fun(n, o) {
console.log(o);
return {
fun: function (m) {
return fun(m, n)
}
}
}
var a=fun(0); a.fun(1);a.fun(2);a.fun(3);
var b=fun(0).fun(1).fun(2).fun(3)
var c=fun(0).fun(1);c.fun(2);c.fun(3)
6.内存溢出与内存泄露
**内存溢出:**当程序运行需要的内存超出了计算机为我么分配到内存空间就会造成内存溢出
**内存泄露:**可以理解为亚健康,和平时的编码的习惯相关,比如占用的内存没有及时释放,并且内存溢出最终造成的局面就是内存溢出,雪崩时没有一片雪花时无辜的。
常见的内存泄露有:
- 过多的局部变量
- 没有清理的订阅任务
- 闭包
六. 面向对象
1.对象有几种创建方式
- Object构造函数模式,通过
new Object - 字面量创建对象
- 工厂模式创建对象(实际上时对字面量创建对象进行了优化,可以创建多个同属性同方法的对象)
- 自定义构造函数模式(这样做实例化多个后所有的属性和方法都会重复出现在内存中)
- 自定义构造函数+原型(构造函数放属性,原型放方法)
- ES6使用class创建对象
2.对象继承的方式有哪几种
其实这里在大方向上分为两种:ES5中继承和ES6中继承。我们主要说一下ES5中的继承
ES5中的继承虽然被说成有很多种,当时相结合起开后,其实只有一种,就是我们的原型链+构造函数继承(组合继承)。
我们直接来说一下种方式的思想吧:实际上就是将父类的实例对象给子类的原型,本来我们的原型是Object的实例对象,但是我们现在将原型替换为父类的实例对象,然而父类的实例对象又指向父类对象方法的原型,父类对象方法的原型又是一个Object,所以说这个操作除了可以让我们拿到父类属性和方法外,再无影响,其实我们主要是想拿到父类原型上的方法。这步操作结束以后我们就可以再子类中使用父类的方法啦;但是同时又出现了一个问题,你有没有考虑过constructor 属性呢,你现在可以想一下子类的constructor属性指向哪里,如果你了解原型的相关知识,很容易推断出此时的constructor指向的是父类的函数对象,因为我们constructor是在原型对象上的,此时子类的原型对象是父类的原型对象,所以此时的constructor指向父类,因此我们还要把丢失的constructor找回来。
为什么不在父类的原型方法上拿属性呢?因为我们如果通过这种方式,那么我们使用父类的属性时,还需要再new父类实例对象时再去传入参数初始化属性,相当麻烦,所以我们就引出来构造函数继承。我们使用借用this的方式拿到父类的属性。
我们直接来看一下实例代码:
function Person(name,age) {
this.name=name;
this.age=age
}
Person.prototype.setname=function(name){
this.name=name
}
function Student(name,age,price) {
Person.call(this,name,age)
this.price=price
}
Student.prototype=new Person()
Student.prototype.constructor=Student;
Student.prototype.setPrice=function(price){
this.price=price
}
let stu=new Student('张三','12','10000');
console.log(stu.name,stu.age,stu.price)
七. this/call/apply/bind
1.对this对象的理解
this是执行上文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this的指向可以通过四种调用模式来进行调用。也对应了函数的函数调用的四种方式。
- 默认绑定,也就是函数的直接独立调用
- 隐式绑定,函数的方法的调用(隐式丢失的情况)
- 显示绑定,通过bind/call/apply调用
- new绑定,也就是构造函数的调用
2.this隐式绑定丢失的情况
首先明确一点什么是隐式丢失,就是指隐式绑定的方法丢失了绑定的对象,从而默认绑定到了window上
1.函数别名
var a=0;
function foo() {
console.log(this.a)
}
var obj={
a:1,
foo
}
var bar=obj.foo
bar()
2.参数传递
var a=0;
function foo() {
console.log(this.a)
}
var obj={
a:1,
foo
}
function bar(fn) {
fn()
}
bar(obj.foo)
3.内置函数
var a=10;
function foo() {
console.log(this.a)
}
var obj={
a:1,
foo
}
setTimeout(obj.foo,500);
4.间接调用
var a = 2;
function foo() {
console.log(this.a)
}
var obj = {
a: 3,
foo
}
var p = { a: 4 };
obj.foo();
(p.foo = obj.foo)()
p.foo()
3.call和apply的区别
传参形式不同,剩下的没有区别 。
**apply:**接受两个参数,第一个就是this对象的指向,第二个参数是一个带下标的集合,这个集合可以为数组,也可以是类数组,最终就这个集合的元素作为参数传递给被调用的函数
**call:**接受若干个参数,第一个参数和apply一样,剩下的参数就是分别依次传入
4.手写call和apply函数
手写call函数:
步骤:
1.判断第一个参数的类型
2.将当前的函数作为传入对象的方法
3.取出argument类数组的值
4.执行obj.当前函数(newarugument),并保存返回值
5.删除传入对象的方法
6.返回返回值
代码如下:
Function.prototype.newCall = function (obj) {
if (typeof obj != "object" || obj == null) { obj = window }
obj.fn = this
let newArguments = [];
for (let i = 1; i < arguments.length; i++) {
newArguments.push(arguments[i])
}
var result = obj.fn(...newArguments)
delete obj.fn
return result
}
手写apply函数:
apply函数和call函数唯一不同的就是参数不同,apply是有两个参数的。我们可以进行判断,如果没有第二个参数那么我们直接执行函数,如果有参数再去 做参数的处理。
代码如下:
Function.prototype.newApply = function (obj, arr) {
var result;
if (typeof obj != "object" || obj == null) obj = window
obj.fn = this
if (arr == null) {
result = obj.fn()
} else {
let newArgument = []
for (let i = 0; i < arr.length; i++) {
newArgument.push(arr[i])
}
result = obj.fn(...newArgument)
}
delete obj.fn;
return result
}
5.手写bind函数
首先我们需要清楚bind函数的作用以及一些特性。bind函数一般返回一个函数,并且具有以下的特性:
- 具有柯里化的特性
- 返回的函数可以进行new实例化(实例化后bind绑定的obj对象this会丢失)
我们来分析一下,事实上如果是单纯的改变this指向,bind 的内部并不复杂,因为它直接使用了apply函数。重要的是我们如何实现它的两个特性。
首先柯里化的特性我们是这样实现的:我们知道在内部我们会返回一个函数,因为我们就有了两个arguments,这两个arguments分别是我们先后传递的参数,我们只需要将这两个的arguments参数处理并且拼接就可以完成了。
接着就是new实例化的操作,首先我们如果使用了new操作,那么我们内部返回的函数的this就会指向这个构造函数的实例,如果不适用指向window,所以我们当其指向构造函数的实例时我们就将this绑定到apply上,如果不是就绑定在之前的obj上。最后再将我们调用函数的原型指向我们返回函数的原型上,这样一来我们相当于做了一个继承的操作,如果我们进行了实例化操作我们就可以拿到person原型上的数据了。
代码实现:
Function.prototype.newBind = function (obj) {
if (typeof obj != "object" || obj == null) obj = window
var that = this
var arr1 = Array.prototype.slice.call(arguments, 1);
var newFun = function () {
console.log(this)
var arr2 = Array.prototype.slice.call(arguments);
if (this instanceof newFun) {
that.apply(this, arr1.concat(arr2))
} else {
that.apply(obj, arr1.concat(arr2))
}
}
newFun.prototype = that.prototype
return newFun;
}
八.异步编程
1.异步编程的实现方式
JavaScript异步机制一般分为一下几种:
- 回调函数:多个回调函数的嵌套会造成回调地狱,不利于代码维护
- Promise:链式调用解决了回调函数的回调地狱
- generator:可以用同步的方法来进行异步的操作
- async函数:是promise和generator实现的一个自动执行的语法糖,返回的是Promise、结构清晰。
2.对Promise的理解
首先Promise是异步编程的一种决解方案,它本身是一个对象,可以获取异步的消息,解决了回调函数引发的回调地狱问题。
Promise可以理解为是一种容器,里面保存着未来才会结束的事件(通常是一个异步操作)的结果。
Promise的实例有三个状态:
- Pending(进行中)
- resolved(已完成)
- rejected(已拒绝)
当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected
Promise的实例有两个过程:
- pending -> fulfilled : Resolved(已完成)
- pending -> rejected:Rejected(已拒绝)
注意这个操作是不可逆的。
总结:
Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。
状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
注意:构造函数内部的代码时立即执行的。
3.对async/await的理解
async/await其实是Generator 和Promise 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中。
async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:
async function testAsy(){
return 'hello world'
}
let result = testAsy()
console.log(result)
result.then(v=>{
console.log(v)
})
那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined) 。
4. async/await 如何捕获异常
使用try..catch
async function fn(){
try{
let a = await Promise.reject('error')
}catch(error){
console.log(error)
}
}
5.async/await对比Promise的优势
-
代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调?也会带来额外的阅读负担 -
Promise传递中间值?常麻烦,?async/await?乎是同步的写法,?常优雅 -
错误处理友好,async/await可以?成熟的try/catch,Promise的错误捕获?常冗余
九.垃圾回收与内存泄露
1.浏览器的垃圾回收机制
1.1 垃圾回收的概念:
**垃圾回收:**JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收机制。
回收机制:
- JavaScript具有自动的垃圾回收机制,会定期对哪些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存
- JavaScript中有两种变量,局部变量和全局变量。全局变量的生命周期会持续到页面的销毁;而局部变量的生命周期随着函数的执行结束就会被回收
- 当局部变量被外部函数使用时,就是我们常说的闭包的情况,这时当函数执行结束后,函数的外部变量依然指向内部函数的局部变量,此时局部变量依然在被使用,所以就不会被回收,需要我们手动置null。
1.2 垃圾回收的方式
浏览器常用的垃圾回收方式有两种:标记清除,引用计数
1.标记清除:
标记清除时浏览器常用的垃圾回收方式,当变量进入到执行环境中时,就会标记这个变量进入环境,被标记为进入环境的变量是不能被回收的,因为它们正在被使用。当变量离开环境时,就会被标记为离开环境,被标记为离开环境的变量会被内存释放。
2.引用计数:
这种方式用的相对较少,引用计数就是跟踪这个值被引用的次数,如果有一个变量将一个引用类型赋值给该变量,那么这个值的引用次数+1。如果包含这个值引用的变量又取得了另外一个值。那这个值的引用次数-1.当它的引用次数为0 时,就会被回收。
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 将被回收
}
注意这种垃圾回收的方式有一种缺点就是循环引用的问题,例如:
obj1和obj2相互引用,计数不可能为0,这时就需要我们手动释放内存。
function fun() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
1.3 减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
-
**对数组进行优化:**在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。 -
对**object** **进行优化:**对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。 -
**对函数进行优化:**在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
2.导致内存泄露的因素
以下四种情况会造成内存的泄漏:
-
**意外的全局变量:**由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。 -
**被遗忘的计时器或回调函数:**设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。 -
**脱离 DOM 的引用:**获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。 -
**闭包:**不合理的使用闭包,从而导致某些变量一直被留在内存当中。
|