终于来搞JS部分了,真的是太多了
在对原型进行解释的时候,会涉及两个概念:构造函数和原型对象;
构造函数:简单的说就是你script
标签里声明的那个函数;
原型对象:在声明了一个函数之后,浏览器会自动按照一个的规则创建一个对象,这个对象就叫做原型对象。这个原型对象其实是存储在内存当中
在声明了一个函数后,这个构造函数(声明了的函数)中会有一个属性prototype,这个属性指向的就是这个构造函数(声明了的函数)对应的原型对象;原型对象中有一个属性constructor,这个属性指向的是这个构造函数(声明了的函数)。
ES5中新增了一个Object.getPrototypeOf()
方法,可以通过这个方法来获取对象的原型
原型链:当我们访问一个对象的属性或方法的时候,首先是在自身上找,如果没有找到,就往上原型上找,原型上找不到,就往原型的原型上找,形成一条原型链。
原型链终点是Object.prototype.__proto__
,而Object.prototype.__proto__ === null(true)
,所以原型链的终点是null
。原型链上的所有原型都是对象,所有的对象都是由Object
构造的,而Object.prototype
的下一级是Object.prototype.__proto__
如何检测一个属性或方法是自身(原型链)所拥有的?——hasOwnProperty()
teacher.hasOwnProperty('name'); // true
this关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。
绑定规则:
var name = 'Jenny';
function person() {
return this.name;
}
console.log(person()); //Jenny
注意:严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined,除非在非严格模式下
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m() // 1
function test() {
this.x = 1;
}
var obj = new test();
obj.x // 1
var x = 0;
function test() {
console.log(this.x); // this指向的是obj对象
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply(obj) // 1
优先级:new绑定 > 显示修改> 隐式绑定 > 默认绑定
执行上下文是对JavaScript代码执行环境的抽象概念,只要有代码运行,那么它一定是运行在执行上下文中。
执行上下文的类型分为三种:
1、全局执行上下文:只有一个,浏览器中的全局对象就是 window对象,this 指向这个全局对象;
2、函数上下文:存在无数个,只要在函数调用的时候才会被创建,每次调用函数都是创建一个新的执行上下文;
3、eval函数执行上下文:指的是运行在eval函数中的代码,很少用而且不建议使用
执行上下文的生命周期:
1、创建阶段
2、执行阶段
执行变量赋值,代码执行
3、回收阶段
执行上下文出栈等待虚拟机回收执行上下文
执行栈,也叫调用栈,具有先进后出的结构,用于存储在代码执行期间创建的所以执行上下文;
相关资料
https://editor.csdn.net/md/?articleId=126385889
事件流都经历三个阶段:事件捕获阶段,处于目标阶段,事件冒泡阶段
事件模型可以分为三个阶段:
1、原型事件模型
事件绑定监听函数比较简单,有两种方式:
2、标准事件模型
3、IE事件模型
typeof
是一个一元运算符,返回是一个字符串,该字符串说明运算数的类型
instanceof
运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。
区别:
1、typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值
2、instanceof可以准确的判断复杂引用数据类型,但不能正确判断基础数据类型
3、typeof可以判断基础数据类型(null除外),但引用数据类型中,除了function类型之外,其他的也无法判断
通用的检测数据类型,可以采用Object.prototype.toString.call()
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(window) //"[object Window]"
事件代理,通俗的讲,就是把一个元素响应事件的函数委托到另一个元素。
应用场景:
如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件,如果给每个列表项都绑定一个函数,那对于内存消耗是非常大的,这个时候就可以事件代理,把点击事件绑定在父级元素ul上去,在执行事件的时候匹配目标元素。
优点:
事件委托的局限性:focus
、blur
无法进行委托绑定事件;mousemove
、mouseout
不适合事件委托
new关键字主要做了一下工作:
1、创建一个新的对象obj({})
2、将空对象的原型,指向于构造函数的原型(proto=Person.prototype)
3、将构建函数中的this绑定到新建的对象obj上
4、根据构建函数返回类型作判断,如果是基本数据类型则被忽略,如果是引用数据类型,new实例失效,需要正常返回
<script>
// 构造函数
function Fun(age,name){
this.age=age;
this.name=name;
return [1,2,4]
}
/* 手写new操作符 */
function create(fn,...args){
// 1、创建一个新的对象obj({})
var obj = {};
// 2、将空对象的原型,指向于构造函数的原型(__proto__=Person.prototype)
obj.__proto__ = fn.prototype;
// 3、将构造函数中的this绑定到新建的对象obj上
var result= fn.apply(obj,args);
// 4、根据构造函数返回类型作判断,如果是基本数据类型则被忽略,如果是引用数据类型,new实例失效,需要正常返回
return result instanceof Object ? result : obj
}
console.log(create(Fun,18,'张三')) // [1,2,4]
</script>
ajax
原理简单的来说通过XmlHttpRequest
对象来向服务器发送异步请求,从服务器获得数据,然后用javaScript
来操作DOM而更新页面。
实现的过程:
1、创建Ajax的核心对象XMLHttpRequest
对象
const xhr = new XMLHttpRequest();
2、通过XMLHttpRequest
对象的open()
方法与服务器建立连接
xhr.open(method, url, [async][, user][, password])
3、通过XMLHttpRequest
对象的 send()
方法发送给服务器端
xhr.send([body])
4、通过 XMLHttpRequest
对象提供的 onreadystatechange
事件监听服务器端的通信状态。readyState
属性有5个值,分别表示不同的请求响应阶段:
5、接受并处理服务端向客户端响应的数据结果
6、将处理结果更新到HTML页面中
status状态码
//封装一个ajax请求
function ajax(options) {
//创建XMLHttpRequest对象
const xhr = new XMLHttpRequest()
//初始化参数的内容
options = options || {}
// toUpperCase() 方法用于把字符串转换为大写
options.type = (options.type || 'GET').toUpperCase()
options.dataType = options.dataType || 'json'
const params = options.data; // 假设这里只有一个参数
//发送请求
if (options.type === 'GET') {
// xhr.open 第三个参数 false是表示同步,true表示异步
xhr.open('GET', options.url + '?' + params, true)
xhr.send(null)
} else if (options.type === 'POST') {
xhr.open('POST', options.url, true)
xhr.send(params)
//接收请求
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { //监听服务器端的状态
let status = xhr.status
if (status >= 200 && status < 300) {
options.success && options.success(xhr.responseText, xhr.responseXML)
} else {
options.fail && options.fail(status)
}
}
}
}
bind、call、apply作用都是改变函数执行时的上下文;
区别:
1、三者都可以改变函数this的对象指向
2、三者第一个参数都是this要指向的对象,如果没有这个参数就为undefined或null,则默认指的是全局window
3、三者都可以传参,但apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入
4、bind是返回绑定this之后的函数,apply,call则是立即执行
JS是一门单线程的语言,意味着同一时间内只能做一件事,但是这不意味着单线程就是阻塞,防止单线程阻塞的方法就是 事件循环。
同步任务与异步任务的运行流程图如下:
同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。
异步任务又分了微任务,宏任务:
微任务(先):
Promise.then,async
MutaionObserver
Object.observer(已废弃;Proxy对象替代)
process.nextTick(Node.js)
宏任务(后):
script
setTimeout/setInterval
UI rendering / UI事件
postMessage、MessageChannel
setImmediate、I/O
DOM 文档对象模型
querySelector 传入任何有效的css选择器,即可选中单个DOM元素(首个)
querySelectorAll 返回一个包含节点子树内所有与之相匹配的Element节点列表,如果没有相匹配的,则返回一个空节点列表
const notLive = document.querySelectorAll("p");
BOM,浏览器对象模型
递归,在数学和计算机科学中,是指在函数的定义中使用函数自身的方法。
如果一个函数内部调用自身,这个函数就是递归函数。
尾递归,即在函数尾位置调用自身,没有常量。
好处:在递归调用的过程中,递归次数过多容易造成栈溢出,而使用尾递归,即一个函数中所有的递归形式调用都出现在函数的末尾,对于尾递归来说,由于只存了一个调用记录,所有永远不会发生“栈溢出”错误。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
// 每次返回的都是一个新的函数,不带上一个函数的参数,也就不需要储存上一个函数了,尾递归只需要
// 保存一个调用栈,复杂度O(1)
应用场景:
1、数组求和
2、优化斐波那契数列
3、数组扁平化
4、数组对象格式化
内存泄漏是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。
内存泄漏,我们可以通过垃圾回收来进行回收。
垃圾回收:JS代码运行时,需要分配内存空间来存储变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
通常垃圾回收的方式有两种实现方式:
但变量进入执行环境时,标记这个变量“进入环境”。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将标记为“离开环境”。
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它将所以在上下文中的变量,已经被在上下文中的变量引用的变量标记去掉。在此之后,带有标记的变量就是待删除的,原因是任何在上下文的变量都访问不到她们了,随后进行垃圾回收程序一次内存清理,销毁带有标记的所有值收回它们的内存。
语言引擎有一张“引用表”,保存了内存中里面的所有资源,如果一个次数为0,就表示这个值不再用到了,因此就可以把这块区域释放。
减少垃圾回收:
常见的内存泄漏情况:
indexedDB是一种低级API,用于存储客户端大量结构化数据。
优点:
1、储存量理论上没有上限
2、所有的操作都是异步的
3、原生支持存储JS对象
4、是个正经的数据库,意味着数据库能做的事情它也能做
缺点:
1、操作非常繁琐
2、本身有一定的门槛
关于cookie、sessionStorage、localStorage三者的区别:
1、存储大小:cookie数据大小不能超过4KB,sessionStora和localStorage存储量可以达到5M或更大;
2、有效时间:locaStorage存储持久,浏览器关闭后数据不会丢失,除非自己手动删除;sessionStorage,浏览器关闭数据也就删除;cookie可以设置过期时间;
3、数据与服务器之间的交互方式:cookie的数据会自动传递给服务器,而sessionStorage,localStorage不会自动把数据发送给服务器,仅在本地保存。
应用场景:
函数式编程是一种“编程范式”,一种编写程序的方法论。JS是一门以函数为第一公民的语言,必定式支持一种编程范式的,以下是JS中的函数式编程
优势:
使用纯函数,可以产生可测试的代码;
不依赖外部环境计算,不会产生副作用,提高函数的复用性;
可读性更强;
可以组转成复杂任务的可能性
高阶函数
高阶函数就是以函数作为输入或者输出的函数被称为高阶函数
柯里化
柯里化是接受多个参数的函数,变成接受一个单一参数,并且返回接受余下的参数。
function sum(x) {
return function(y) {
return function(z) {
return x + y + z
}
}
}
const res = sum(10)(20)(30)
console.log(res)
特点:
1、让函数的职责更加单一。柯里化可以实现让一个函数处理的问题尽可能的单一,而不是将一大堆逻辑交给一个函数来处理。
2、提高函数参数逻辑复用
函数式编程的优点;
1、更好的管理状态
2、更简单的复用
3、更优雅的组合
4、隐性好处
函数式编程的缺点:
1、性能
2、资源占用
3、递归陷阱
函数缓存,就是将函数运算过的结果进行缓存,本质上就是用空间换时间。
实现函数缓存主要依靠闭包,柯里化,高阶函数。
应用场景:
1、对于昂贵的函数调用,执行复杂计算的函数
2、对于具有有限且高度重复输入范围的函数
3、对于具有重复输入值的递归函数
4、对于纯函数,即每次使用特定输入调用时返回相同输出的函数
0.1+0.2 === 0.3 // false
计算机内部式把十进制转换为二进制去进行计算,所以
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
// 转成十进制正好是 0.30000000000000004 ,与0.3不相等
解决方法:
parseFloat(result.toFixed(12)) 或 parseFloat(result.toPrecision(12))
2^-52
在ES6中,提供了Number.EPSILON
属性,而它的值也是2^-52
,只需要判断 0.1+0.2-0.3是否小于Number.EPSILON
,如果小,就可以判断为 0.1+0.2 === 0.3function numberepsilon(arg1,arg2){
return Math.abs(arg1 - arg2) < Number.EPSILON;
}
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
节流:n秒内只运行一次,若在n秒内重复触发,只有一次生效;
防抖:n秒后在执行该时间,若在n秒内被重复触发,则重新计时.
比如:
电梯第一个人进来后,15秒后准时运送一次,这是节流;
电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖.
区别:
1、函数防抖,在一段连续操作结束后,处理回调。利用clearTimeout和setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次;
2、函数防抖关注一定时间连续触发的事件,只在最后执行一次。而函数节流一段时间内只执行一次,多次变成少次。
应用场景:
防抖:
节流:
分片上传:
分片上传,就是将多要上传的文件,按照一定的大小,将整个数据块来进行分片上传。上传之后再由服务端对所有上传的文件进行汇总整合成原始的文件。
断点续传:
断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分。每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没必要从头开始上传下载。用户可以节省时间,提高速度。
应用场景:
上拉加载
scrollTop:滚动视窗的高度距离window顶部的距离
clientHeight:它是一个定值,表示屏幕可视区域的高度
scrollHeight:页面不能滚动时也是存在的,此时scrollHeight等于clientHeight。
scrollHeight表示body所有元素的总长度(包括body元素自身的padding)
触底公式
scrollTop + clientHeight >= scrollHeight
下拉刷新
主要分成三步:
1、监听原生touchstart
事件,记录其初始位置的值,e.touches[0].pageY
;
2、监听原生touchmove
事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的translateY
属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;
3、监听原生touchend
事件,若此时元素滑动达到最大值,则触发callback
,同时将translateY
重设为0,元素回到初始位置。
第三方库:better-scroll
better-scroll
是一款重点解决移动端(已支持PC)各种滚动场景需求的插件。
单点登录,简称SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
比如:
淘宝,天猫都属于阿里旗下,当用户登录淘宝后,再打开天猫,系统便自动帮助用户登录了天猫,这种现象就属于单点登录。
实现:
【同域名下的单点登录】
将cookie
的domain
属性设置为父域的域名(主域名),同时将cookie
的path
属性设置为根路径,将SessionID(或Token)
保存到父域中,这样所有的子域应用都可以访问到这个cookie
【不同域名下的单点登录】
第一种方法:
(1)用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 token 写入 Cookie(注意这个 Cookie是认证中心的,应用系统是访问不到的)
(2)应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心.由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了
(3)如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录;如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL,并在跳转前生成一个 Token,拼接在目标URL 的后面,回传给目标应用系统
(4)应用系统拿到 Token之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token写入Cookie,然后给本次访问放行。
此实现方法相对复杂,支持跨域,扩张性好,是单点登录的标准做法。
第二种方法:
前端拿到Token
后,处理将它写入自己的LocalStorage
中之外,前端通过 iframe+postMessage()
方式,将同一份 Token
写入到了多个域下的 LocalStorage
中,前端每次在向后端发送请求之前,都会主动从 LocalStorage
中读取Token
并在请求中携带,这样就实现了同一份Token
被多个域所共享.
XSS跨站脚本攻击
CSPF跨站请求伪造
SQL注入攻击
Sql注入攻击,是通过将恶意的sql查询或添加语句插入到应用的输入参数中,再在后台Sql服务器上解析执行进行的攻击。
预防:
1、严格检测输入变量的类型和格式;
2、过滤和转义特殊字符;
3、对访问数据库的web应用程序采用Web应用防火墙
JavaScript共有八种数据类型,分别是Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
Symbol代表创建后独一无二不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
BigInt是一种能够表示任意精度,安全地存储和操作的大整数的数字类型。即使这个数已经超出了Number能够表示的安全整数范围
两种类型的区别在于存储位置的不同:
1、原始数据直接存储在栈中的简单数据段,占据空间大小,大小固定,属于被频繁使用数据,所以放入栈中存储;
2、引用数据类型存储在堆中的对象,占据空间大,大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
(1)typeof
console.log(typeof null); // object
(2)instanceof
console.log(2 instanceof Number); // false
(3)constructor
console.log((2).constructor === Number); // true
(4)Object.prototype.toString.call()
var a = Object.prototype.toString;
console.log(a.call(2)); //[object Number]
注意:obj.toString()
结果和Object.prototype.toString.call(obj)
的结果不一样,这是为什么?
Object.toString()//"function Object() { [native code] }"
Object.prototype.toString()//"[object Object]"
Object
输出的是其函数体"function Object() { [native code] }"
而Object
原型上输出的是其类型。我们可以看出Object
对象和它的原型链上各自有一个toString()
方法,第一个返回的是一个函数,第二个返回的是值类型。而Array
,function
等类型作为Objec
t的实例,都重写了toString
方法。不同的对象类型调用的toString
方法是,调用的不是对象原型上的方法,而是重写的toString
方法。所以采用obj.toString()
不能得到其对象模型,只是将obj
转换为字符串类型;因此想要得到对象的具体类型时,应该调用Object.prototype.toString()
方法
<script>
let obj = [1, 2, 4, 5]
/* 1、通过Object.prototype.toString.call() */
console.log(Object.prototype.toString.call(obj).slice(8, -1) === 'Array') // true
/* 2、通过原型链做判断 */
console.log(obj.__proto__ === Array.prototype)// true
/* 3、通过ES6的Array.isArray()做判断 */
console.log(Array.isArray(obj))// true
/* 4、通过instanceof做判断 */
console.log(obj instanceof Array)// true
/* 5、通过Array.prototype.isPrototypeOf */
// isPrototypeOf()方法返回的一个对象是否存在于另外一个对象的原型链上
console.log(Array.prototype.isPrototypeOf(obj))// true
</script>
区别:
1、Undefined
和Null
都是基本数据类型,这两个基本数据类型分别都只有一个值,就是undefined和null
;
2、undefined
代表的含义是未定义,null
代表的含义是空对象;一般变量声明了但还没有赋值的时候就会返回undefined
,null
主要用于赋值给一些可能会返回对象的变量,作为初始化;
3、undefined
不是JS中的一个保留字,但最好还是不要使用undefined
作为变量名;typeof(null)
返回值是object
,这是一个历史遗留的问题。
undefined
与undeclared
的区别?
1、undefined
:在作用域中声明过但是没有赋过值的变量;
2、undeclared
:没有在作用域中声明过的变量
3、undefined
是JS中的语言类型之一,访问一个undefined
的变量时,浏览器不会报错并且返回undefined
;undeclared
是JS中的一种语法错误,访问一个undeclared
的变量时,浏览器会报错,JS执行会中断。
typeof
是一个一元运算,它返回值是一个字符串,该字符串说明运算数的类型。
typeof null
的结果是"Object"
。
在JavaScript第一个版本中,所以值都存储在32位单元中,类型标签存储在每个单元的低位中,共有五种数据类型。
000 :object 当前存储的数据指向一个对象
001:int 当前存储的数据是一个31位的有符号整数
010:double 当前存储的数据指向一个双精度的浮点数
100:string 当前存储的数据指向一个字符串
110:boolean 当前存储的数据是布尔值
null
的类型标签也是000,和Object
的类型标签一样,所以会被判定为Object
。
instanof运算符用于判断构造函数prototype属性是否出现在对象的原型链中的任意位置。
function myInstanceof(left,right){
// 获取对象的原型
let proto = Object.getPrototypeOf(left)
// 获取构造函数的prototype对象
let prototype = right.prototype;
}
// 判断构造函数的prototype对象是否在对象的原型链上
while(true){
if(!proto) return false;
if(proto===prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法来获取指定对象的原型
proto = Object.getPrototypeOf(proto)
}
undefined
是一个标识符,所以可以被当作变量来使用和赋值。void
并不会改变表达式的结果,只是让表达式不返回值,因此可以用void 0
来获得undefined
。
NaN指的不是一个数字,用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败返回结果”
console.log(typeof NaN) // number
isNaN
函数会首先进行Number
函数转换,如果转换后为number
类型,则返回false
,否则返回true
;
console.log(isNaN(12)) //false
console.log(isNaN(true)) //false
Number.isNaN
函数是严格等于NaN,
不需要任何转换,只有NaN
才能为true
,剩下的全部都是false
console.log(Number.isNaN(NaN)) //true
console.log(Number.isNaN("hhh")) //false
对于 == 来说,如果对方的类型不一样,就会进行类型转换,如果不同,就会进行如下流程:
1、首先判断两者类型是否相同,相同的话就比较两者的大小;
2、类型不相同的话,就进行类型转换;
3、会判断是否在对比null
和undefined
,是的话就返回true
;
4、判断两者类型是否为string
和 number
,是的话就会将字符串转换为number
;
5、判断一方是否为boolean
,是的话就会把boolean
转为number
,再去进行判断;
6、判断其中一方是否为object
且另一方为string
,number
或者symbol
,是的话就会调用ToPrimitive()
内部函数(内部详细转换还需看下37题),将object
转为字符串进行判断。
===
、==
的区别?1、使用 ==
进行判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较;
2、使用 ===
进行相等判断时,不会强制类型转换,直接返回false;
3、使用Object.is()
进行判断时,一般情况和===
相同,它处理了一些特殊的情况,比如-0和+0不再相等,两个NaN
是相等.
console.log(-0 === 0) // true
console.log(Object.is(-0,0)) // false
console.log(Object.is(NaN,NaN)) // true
JavaScript中共有6种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol;
基本数据类型的值不是对象,因此从逻辑上讲它们不应该有方法的属性,然后事实并不是我们所想的那样。为了方便操作基本数据类型的值,JavaScript中的原始数据类型的值会在后台隐式地包装为对象,从而引出了基本包装类型的概念。
ECMAScript还提供了4个包装类型:Boolean、Number、String、Symbol;
分析以上代码
var str = "hello world";
str.length; // 11
str.toUpperCase(); // HELLO WORLD
执行第二行时:创建String类型的一个实例,在实例上调用指定的属性,再销毁该实例
执行第三行时:创建String类型的一个实例,在实例上调用指定的属性,再销毁该实例
引用类型和基本包装类型的区别:对象的生存期
使用 new
操作符创建的引用类型的实例,在执行流离开当前作用域之前,会一直保存在堆内存中。而后台自动创建的基本包装类型的对象,则只存在一行代码的执行瞬间,然后立即被销毁。这意味着我们不能为基本类型的值添加属性和方法。
ToPrimitive
是javaScript
中每个值隐含的自带的方法,用来将值(无法是基本类型值还是对象)转为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:
/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj,type)
typ的值为number或string
(1)当type为number时规如下:
调用obj的valueOf方法,如果为原始值,则返回原始值,否则下一步;
调用obj的toString方法,如果为原始值,则返回,否则下一步;
抛出TypeError 异常。
(2)当type为string时规则如下:
调用obj的toString方法,如果为原始值,则返回,否则下一步;
调用obj的valueOf方法,如果为原始值,则返回,否则下一步;
抛出TypeError 异常。
总结:看出两者的主要区别在于调用toString和valueOf的先后顺序。默认情况:
如果对象为Date对象,则type默认为string,先调用toString方法;
其他情况下,type默认为number,调用valueOf方法;
以下是基本数据类型的值在不同操作符的情况下隐式转换的规则:
1、+
操作符,两边有至少一个string类型变量时,两边的变量都会被隐式转为字符串,其他情况转为数字;
2、- * 、\
操作符NaN也是一个数字;
3、==
操作符,两边尽量转为number
;
4、< >
比较符,如果两边都是字符串,则比较字母表顺序,其他情况转为数字再比较;
5、如何是对象比较,会先转为基本类型在进行转换:
var a = {}
a > 2 // false
解析过程:
1、a不是Date对象,所以type为number
2、a.valueOf() // 结果还是个对象
3、a.toString() // "[object Object]",现在是一个字符串了
4、比较符 > 其他情况会转为数字 NaN > 2 // false 得出结果
Javascript中有Number.MAX_SAFE_INTEGER
表示最大安全数字,计算结果是9007199254740991
,即在这个数范围内不会出现精度丢失(小数除外)。但是一旦超过这个范围,js就会出现计算不准确的情况,这在大量计算的时候不得不依靠一些第三方库来解析bignumber.js
处理大整数,所以官方就提出BigInt
来解决此问题。
创建BigInt,只需要在整数末尾追加n即可
console.log(9007199254740995n); //9007199254740995n
console.log(9007199254740995); //9007199254740996 出现精度问题
或者调用BigInt()
构造函数
BigInt("9007199254740995"); //9007199254740995n
// 扩展运算法
let outObj ={inObj:{a:1,b:2}}
let newObj = {...outObj}
newObj.inObj.a=2;
console.log(outObj) // inObj: {a: 2, b: 2}
// object.assign
let outObj ={inObj:{a:1,b:2}}
let newObj = Object.assign({},outObj)
newObj.inObj.a=2;
console.log(outObj) // inObj: {a: 2, b: 2}
两者都是浅拷贝,会修改原来的数据。
1、Object.assign()
方法接受的第一个参数作为目标对象,第二个参数作为源对象。然后把所有源对象合并到目标对象中。它修改了一个对象,就会触发ES6 的setter;
2、扩展操作符(…)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中,它不复制继承的属性或类的属性,但是它会复制ES6的symbol属性(表示独一无二的值,最大的用法来定义对象的唯一属性名)
JSON.stringify
方法来判断let Obj ={}
if (JSON.stringify(Obj) == '{}') {
console.log('空对象');
}
Object.keys()
来判断let Obj ={}
// Object.keys()方法会返回一个由给定对象的自身可枚举属性组成的数组
if (Object.keys(Obj).length == 0) {
console.log('空对象');
}
(1)块级作用域:块作用域由{ }
包括,let
和const
具有块级作用域,var
不存在块级作用域;
(2) 变量提升:var
存在变量提升,let
和const
不存在变量提升,即在变量只有声明之后使用,否在会报错;
(3)给全局添加属性:浏览器的全局对象是window
,Node
的全局对象是global
。var
声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let
和const
不会;
(4)重复声明:var
可以重复声明变量,后面声明的同名变量会覆盖之前声明的变量。const
和let
不允许重复声明变量;
(5)暂时性死区:在使用let
、const
声明变量之前,该变量都是不可用的。在语法上称为暂时性死区。使用var
声明的变量不存在暂时性死区;
(6)初始值设置:在变量声明时,var
和let
可以不要设置初始值,而const
声明变量必须设置初始值;
(7)指针指向:let
创建的变量时可以更改指针指向(可以重新赋值),但const
声明的变量是不允许改变指针的指向。
const
定义的如果是基本数据类型,定义后就不可再修改,如果修改,就会报错;
但如果是对象的属性,const
定义的对象中保存的是指向对象的指针,这里的“不变”是指向对象的指针不变,而修改对象中的属性并不会让指向对象的指针发生变化,所以用const
定义对象,对象的属性是可以改变的。
箭头函数是ES6
中提出来的,它没有prototype
,也没有自己的this
指向,更不可以使用arguments
参数,所以不能New
一个箭头函数。
(1)箭头函数比普通函数更加简洁;
(2)箭头函数没有自己的this
,没有自己的arguments
,没有prototype
,不能作为构造函数使用;
(3)箭头函数继承来的this指向永远不会改变,call()、apply()、bind()
等方法不能改变箭头函数中的this
的指向;
(4)箭头函数不能用作Generator
函数,不能使用yield
关键字的阻塞原理.
扩展运算符,是将数组转为分割的参数序列。
(1)对象的扩展运算符(…)用于取出参数对象中所有可能遍历属性,拷贝到当前对象之中;
let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 } 等价于 let baz = Object.assign({}, bar);
注意:如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖;
let bar = {a: 1, b: 2};
let baz = {...bar, ...{a:2, b: 4}}; // {a: 2, b: 4}
(2)数组的扩展运算符可以将一个数组转为用空格分隔的参数序列,且每次只能展开一层数组;
console.log(...[1, 2, 3])
// 1 2 3
console.log(...[1, [2, 3, 4], 5])
// 1 [2, 3, 4] 5
数组的扩展运算符的应用:
1、将数组转换为参数序列
function add(x, y) {
return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3
2、复制数组
const arr1 = [1, 2];
const arr2 = [...arr1];
3、合并数组
const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five'];
// ["one", "two", "three", "four", "five"]
4、扩展运算符与解构赋值结合起来,用于生成数组
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
(注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错)
const [...rest, last] = [1, 2, 3, 4, 5]; // 报错
const [first, ...rest, last] = [1, 2, 3, 4, 5]; // 报错
5、将字符串转为真正的数组
[...'hello'] // [ "h", "e", "l", "l", "o" ]
6、使用Math函数获取数组中特定的值
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Math.max(...numbers); // 9
解构是ES6提供的一种新的提取数据的模式,这种模式能够从对象或数组里有针对性地拿到想要的数值。
1、数组的解构
const [a,b,c] = [1,2,3]
console.log(a,b,c) // 1 2 3
const [a,,c] = [1,2,3]
console.log(a,c) // 1 3
2、对象的解构
在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。
const stu = {
name: 'Bob',
age: 24
}
const { name, age } = stu
console.log(name,age) // Bob 24
注意:对象解构严格以属性名作为定位依据,所以就算调换了name和age的位置,结果也是一样的。
如何提取高度嵌套的对象里面的指定属性?
const school = {
classes: {
stu: {
name: 'Bob',
age: 24,
}
}
}
像此处的 name 这个变量,嵌套了四层,此时如果仍然尝试老方法来提取它:
const { name } = school
显然是不奏效的,因为 school 这个对象本身是没有 name 这个属性的,name 位于 school 对象的“儿子的儿子”对象里面。
要想把 name 提取出来,一种比较笨的方法是逐层解构:
const { classes } = school
const { stu } = classes
const { name } = stu
name // 'Bob'
但是还有一种更标准的做法,可以用一行代码来解决这个问题:
const {classes:{stu:{name} }} =school
console.log(name) // Bob
ES6中引入rest
参数(形式为’…变量名’),用于获取函数的多余参数,或者处理函数参数个数不确定的情况。
unction mutiple(...rest) {
onsole.log(rest) // [1, 2, 3, 4]
}
mutiple(1, 2, 3, 4)
传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 rest 的值,会发现它是一个数组;
rest参数与扩展运算符有点类似,但是rest参数的输出形式是数组,而扩展运算符输出的形式是用空格分隔的参数序列
扩展运算符:
console.log(...[1, 2, 3])
// 1 2 3
ES6以前都是有拼接字符串+,现在提出“模板语法”概念,字符串更加容易拼了,也更易读了,代码整体的质量都变高了。
var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`
模板字符串的优势:
1、运行使用${}方式嵌入变量
2、在模板字符串中,空格,缩进,换行都会被保留
3、模板字符串完全支持“运算”式的表达式,可以在${}里面完成一些计算
ES6新增的字符串方法:
const son = 'haha'
const father = 'xixi haha hehe'
father.includes(son) // true
const father = 'xixi haha hehe'
father.startsWith('haha') // false
father.startsWith('xixi') // true
const father = 'xixi haha hehe'
father.endsWith('hehe') // true
const sourceCode = 'repeat for 3 times;'
const repeated = sourceCode.repeat(3)
console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;
map本质上就是键值对的集合。
Map数据结构有一下操作方法:
size:map.size 返回Map结构的成员总数
set(key,value) 设置键名key对应的键值value,返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。
get(key) 该方法读取key对应的键值,如果找不到key,返回undefined
has(key) 该方法返回一个布尔值,表示某个键是否在当前Map对象中
delete(key) 该方法删除某个键,返回true,如果删除失败,返回false
clear() map.clear()清除所有成员,没有返回值。
Map结构原生提供是三个遍历器生成函数和一个遍历方法:
keys() 返回键名的遍历器
values() 返回键值的遍历器
entries() 返回所有成员的遍历器
forEach() 遍历Map的所有成员
weakMap
对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始的数据类型不能作为key值,而值可以是任意的。
强引用:对象在JavaScript中是强引用,也就是将一个引用对象通过变量保存下来,那么这个变量或者常量就是强引用,这个对象不会被回收;
弱引用:将一个对象作为键添加到WeakMap或WeakSet中并不能防止这些对象被回收。
比如:
区别:
1、WeakMap
的键名只支持对象,Map
的键名可以是任意值;
2、由于WeakMap
的成员随时可能被垃圾回收机制回收,成员的数量不稳定,没有size
属性。WeakMap
不能遍历,Map
可以遍历
3、WeakMap
是弱引用,成员随时可以消失,可以防止内存泄漏
JSON是一种基于文本的轻量级的数据交换格式。它可以被任何的编译语言读取和作为数据格式来传递。
JSON的语法是基于JS的,因此很容易将JSON和JS中的对象弄混,但是应该注意的是JSON和JS中的对象不是同一回事。JSON中对象格式更加严格,比如说在JSON中属性值不能为函数,不能出现NaN这样的属性值等。
在JS中提供了两个函数来实现JS数据结构和JSON格式的转换处理
JSON.stringify() 函数:将JSON数据结构转为JSON格式的字符串
JSON.parse() 函数:将JSON字符串转为JSON对象
延迟加载就是等页面加载完成之后再加载JS文件。JS延迟加载有助于提高页面加载的速度。
1、defer属性:文档解析完成后再解析执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了defer属性的脚本按规范来说最后是顺序执行的。
2、async属性:这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行js脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个async属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
3、动态创建DOM方法:动态创建DOM标签的方式,可以对文档的加载事件进行监听,当文档加载完后再动态的创建script标签引入js脚本
4、使用setTimeout延迟方法:设置一个定时器来延迟加载js脚本文件
5、让JS最后加载:将js脚本放到文档的底部,来使js脚本尽可能的在最后来加载执行
一个拥有length属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。
常见的类数组对象有arguments
。
常见的类数组转换为数组的方式如下:
1、通过call
调用数组的slice
方式来实现转换
Array.prototype.slice.call(arratLike)
2、通过call
调用数组的splice
方式来实现转换
Array.prototype.splice.call(arrayLike,0)
3、通过apply
调用数组的concat
方法来实现转换
Array.prototype.concat.apply([],arrayLike)
4、通过Array.from
方法来实现转换
Array.from(array)
ASCII
码称为美国标准信息码,它是专门为英语而设计的,有128
个编码,对其他语言无能为力,它表示的编码有限,要想表示其他语言的编码,还是要使用Unicode
来表示,可以说Unicode
是ASCII
的超集。
Unicode
叫做统一码,万国码,单一码。Unicode
是为了解决传统的字符编码方案的局限而产生的。
Unicode
的实现方式(也就是编码方式)有很多种,常见的是UTF-8、UTF-16、UTF-32和USC-2
;
Unicode、UTF-8、UTF-16、UTF-32有什么区别?
1、Unicode
是编码字符集,而UTF-8、UTF-16、UTF-32
是字符集编码
2、UTF-16
使用变长码元序列的编码方式,比较复杂、因为其引入了独特的代理。
3、UTF-8
需要判断每个字节中的开头标记信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错,而UTF-16
不会判断开头标记,即使错也只会错一个字符,所以容错能力较强;
4、如果字符内容全部英文或者英文与其他文字混合,但是英文占绝大部分,那么用UTF-8
就比UTF-16
节省了很多空间;那如果字符内容全部是中文这类似的字符或者混合字符中中文占绝大多数,那么UTF-16
就占优势了,可以节省很多空间。
取反运算符(~)
定义:参加运算的一个数据按二进制进行“取反”运算
总结:对一个二进制数按位取反,即将0变1,1变0,当发现按位取反为负数时,就直接取其补码,变成十进制
~6 = ~(0000 0110) = 1111 1001
当发现按位取反为负数时,就直接取其补码
1111 1001
反码:1000 0110
补码:1000 0111 (-7)
因此~6的值为-7
arguments
是有length
属性和若干个索引的对象,与数组相似,但是没有数组常见的方法属性。
要遍历类数组,有三种方法:
1、Array.prototype.forEach.call(arguments,a =>{
console.log(a)
})
2、const arrArgs = Array.from(arguments)
arrArgs.forEach(a =>console.log(a))
3 、const arrArgs = [...arguments]
arrArgs.forEach(a =>console.log(a))
1、escape
是对字符串进行编码(而另外两种是对URL),作用是让它们在所有电脑上可读。
2、encodeURI
、encodeURIComponent
都是编码URL,唯一的区别就是编码的字符范围。encodeURI
方法不会对下列字符编码ACSII字母、数字、~!@#$&()=:/,;?+’ encodeURIComponent
方法不会对下列字符编码ASCII字母、数字、~!()’ encodeURICompont
比encodeURL
编码的范围更大。
(简单的说encodeURI
编译URL
的空格,其他的不编译,encodeURIComponent
编译URL的所有字符 )
变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
造成变量声明提升的本质原因是 JS引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,就会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象、这个对象是在代码解析的时候创建的。
为什么会进行变量提升呢?
1、提高性能
在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
2、容错性更好
我对模块的理解是,一个模块是实现一个特定功能的一组方法。在最开始的时候,js 只实现一些简单的功能,所以并没有模块的概念,但随着程序越来越复杂,代码的模块化开发变得越来越重要。
由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污染,并且模块间没有联系。
后面提出了对象写法,通过将函数作为一个对象的方法来实现,这样解决了直接使用函数作为模块的一些缺点,但是这种办法会暴露所有的所有的模块成员,外部代码可以修改内部属性的值。
现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。
require()
是同步加载模块,ES6模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段;use strict是一种ES5添加的运行模式(严格模式),这种模式使得JS在更严格的条件下运行。设置严格模式的目的如下:
1、消除JS的语法的不合理、不严谨之处,减少怪异行为;
2、保证代码运行的安全
3、提高编译器效率,增强运行速度
4、为未来新版本的JS做好铺垫
区别:
1、禁止使用with语句
2、禁止this关键字指向全局对象
3、对象不能有重名的属性
1、instanceof 判断构造函数的prototype属性是否出现在对象的原型链中的任意位置
2、constructor 判断对象的constructor属性指向该对象的构造函数,但是这种方式不是很安全,因为constructor属性可以被修改
3、Object.prototype.toString() 来打印对象Class属性来判断
1、强类型语言:强类型语言也称强类型定义语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。Python、Java和C++等语言都是强制类型定义的,也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。
2、弱类型语言:JavaScript语言属于弱类型语言。简单理解就是一种变量类型可以被忽略的语言。比如:在JavaScript中就可以把字符串‘12’和整数3进行连接得到字符串‘123’,在相加的时候会进行强制类型转换。
3、两者对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。
1、解释型语言:一边转换一边执行的,其不会由源代码生成可执行文件,而是先翻译成中间代码,再由解释器对中间代码进行解释运行,每执行一次都要翻译一次。
特点总结:
2、编译型语言:在程序在执行之前需要一个专门的编译过程,通过编译器把程序编译成为可执行文件,再由机器运行这个文件,运行时不需要重新翻译,直接使用编译的结果就行了。
特点总结:
for……in 主要是遍历对象的键名,遍历字符串的下标,遍历数组的下标
for……of 主要是遍历数组,遍历字符串的字符,遍历数组的值,不能遍历对象,一般适合遍历数组
for...of
是作为ES6
新增的遍历方式,允许遍历一个含有iterator
接口的数据结构(数组,对象等)并且返回各项的值,普通对象用for...of
遍历是会报错的。
length
属性),用Array.from()
转成数组即可; <script>
var obj = {
0: 'one',
1: 'two',
length: 2
};
obj = Array.from(obj);
for (var k of obj) {
console.log(k) // one
// two
}
</script>
[Symbol.iterator]
属性,并指向一个迭代器即可;方法一:
<script>
var obj = {
a: 1,
b: 2,
c: 3
};
obj[Symbol.iterator] = function () {
var keys = Object.keys(this);
var count = 0;
return {
next() {
if (count < keys.length) {
return { value: obj[keys[count++]], done: false };
} else {
return { value: undefined, done: true };
}
}
}
};
for (var k of obj) {
console.log(k);
/*
1
2
3
*/
}
</script>
方法二
var obj = {
a: 1,
b: 2,
c: 3
};
obj[Symbol.iterator] = function* () {
var keys = Object.keys(obj);
for (var k of keys) {
yield [k, obj[k]]
}
};
for (var [k, v] of obj) {
console.log(k, v);
/*
a 1
b 2
c 3
*/
}
ajax是一种无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量的数据交换,Ajax可以使用网页实现异步更新。
缺点:
1、本身针对MVC编程,不符合前端MVVM的浪潮;
2、基于原生的XHR开发,XHR本身的架构不清晰;
3、不符合关注分离的原则;
4、配置和调用方法非常混乱;
fetch号称是ajax的替代品,是在ES6出现,使用ES6的Promise对象。代码结构比ajax简单多,使用原生js,没使用XMLHttpRequest对象;
**优点:**
1、语法简洁,更加语义化;
2、基于标准Promise实现,支持async/await;
3、更加底层,提供丰富的API;
**缺点:**
1、fetch只对网络请求报错,对400,500都当作成功的请求,服务器返回400,500错误代码时并不会`reject`(拒绝,否定),只有网络错误这些导致请求不能完成,fetch才会被`reject`;
2、fetch默认不会带cookie,需要添加配置项;
3、fetch没办法原生检测请求的进度,而XHR可以
Axios是一种基于Promise
封装的HTTP
客户端
特点:
1、浏览器发起XMLHttpRequests
请求;
2、node
端发起http请求;
3、支持Promise API
;
4、监听请求和返回;
5、对请求和返回进行转化;
6、取消请求;
7、自动转换json数据;
8、客户端支持抵御XSRF
攻击
这两个方法都是用来遍历数组的,区别如下:
forEach()
方法没有返回值,如果它数组的数据为基本数据类型,它是不会改变原数组;如果它数组的数据为引用数据类型,则会修改原数组。 // 数组中是基本数据类型——不修改原数组
const array = [1, 2, 3, 4];
array.forEach(ele => {
ele = ele * 3
})
console.log(array); // [1,2,3,4]
// 数组中是引用数据类型——修改原数组
const objArr = [{
name: 'wxw',
age: 22
}, {
name: 'wxw2',
age: 33
}]
objArr.forEach(ele => {
if (ele.name === 'wxw2') {
ele.age = 88
}
})
console.log(objArr); // [{name: "wxw", age: 22},{name: "wxw2", age: 88}]
map()
方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组处理之后的值;addEventListener
用于事件监听,有三个参数
target.addEventListener(type, listener, useCapture);
参数如下:
type 表示监听事件类型的字符串 比如:‘click’
listener 当所监听的事件类型触发时,会接收到一个事件通知(实现了Event接口的对象),必须是一个接口对象或者是一个函数
useCapture 可选参数 默认为false即冒泡传递,当值为true时,事件使用捕获传递
比如:document.getElementById("myDiv").addEventListener("click", myFunction, true);
闭包是指有权访问另一个函数作用域中的变量的函数。
闭包有两个常用的用途:
1、函数外部能够访问到函数内部的变量,可以使用这种方法来创建私有变量;
2、已经运行结束的函数上下文中的变量对象继续保留在内存中,因为闭包函数保留了这个变量对象的引用,所有这个变量对象不会被回收。
比如:函数A内部有一个函数B,函数B可以访问到函数A的变量,那么函数B就是闭包
function A() {
let a = 1
window.B = function () {
console.log(a)
}
}
A()
B() // 1
经典面试题:循环中使用闭包解决var定义函数的问题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000) // 6 6 6 6 6 6
}
因为setTimeout是异步函数,所以会先把循环全部执行完毕,这时候i就是6了,所以会输出5个6。
解决办法有三种:
第一种使用立即指向函数的方式
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j)
}, 1000)
})(i)
}
第二种使用setTimeout的第三个参数,这个参数会被当成timer函数的参数传入
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},1000,i // 把i传入timer函数中赋值给j
)
}
第三种使用let定义i来解决问题,也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 1000)
}
补充:JS函数的几种写法
#1、常规写法
// 函数的定义
function foo() {
alert('常规写法');
}
// 函数的调用
foo()
#2、匿名函数写法
// 函数的定义
var foo = function(){
alert('匿名函数定义');
}
// 函数的调用
foo()
#3、将方法作为一个对象
// 定义
var test = {
fun1: function(){ },
fun2: function(){ }
}
// 调用
test.fun1();
test.fun2();
#4、自执行函数
(function(){...}) () 或
(function(){...} ())
作用域是变量有效的范围。
1、全局作用域
(1)最外层函数定义的变量拥有全局作用域;
(2)所有未定义直接赋值的变量自动声明为全局作用域;
(3)所有window
对象的属性拥有全局作用域;
(4)全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突.
2、函数作用域
(1)函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到;
(2)作用域是分层的,内层作用域可以访问到外层作用域,反之不行.
3、块级作用域
(1)使用ES6
中新增的let
和const
指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中创建(由{}
包裹的代码片段)
(2)let
和const
声明的变量不会由变量提升,也不可以重复声明
作用域链:如果在自己的作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window
对象就被终止,这一层层的关系就是作用域链。
作用:保证对执行环境有权访问的所有变量和函数,通过作用域链,可以访问到外层环境的变量和函数。
回调函数
多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护
Promise
使用Promise
的方式可能将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个then
的链式调用,可能会造成代码的语义不够明确
Generator
Generator
函数是ES6
新增的一种异步编码的解决方案,语法和传统的函数完全不同;
Generator
函数最大的特点就是可以交出函数的执行权(即暂停执行)。
async函数
async
函数是generator
和promise
实现的一种自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise
对象,那么函数将会等待 promise
对象的状态变为 resolve
后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
Promise
是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息。它的出现避免了地狱回调。
(1)Promise
的实例有三个状态:
Pending
(进行中)Resolved
(已完成)Rejected
(已拒绝)当把一件事件交给Promise
时,它的状态为Pending
,任务完成了状态变成了Resolved
、没有完成失败了就变成Rejected
。
(2)Promise
的实例有两个过程:
注意:一旦从进入状态变成为其他状态就永远不能更改状态了。
Promise的特点:
1、对象的状态不受外界的影响。
3、一旦状态改变就不会再变,任何时候都可以得到这个结果
Promise的缺点:
1、无法取消Promise
,一旦新建它就会立即执行,无法中途取消
2、如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部
3、当处于pending
状态时,无法得知目前进展到那个阶段(刚刚开始还是即将完成)
Promise构造函数
接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
一般情况都是会用new Promise()
来创建promise
对象,但是也可以使用promise.resolve
和promise.reject
这两个方法:
Promise.resolve(11).then(function (value) {
console.log(value); // 打印出11
});
Promise.reject(new Error(“我错了,请原谅俺!!”));
Promise方法:
Promise有5个常用的方法:then()
、catch()
、all()
、race()
、finally()
。
1、then()
then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数时Promise
对象的状态变为rejected
时调用。其中第二个参数可以省略。then
方法返回的是一个新的Promise
实例(不是原来的那个Promise
实例)
Promise.then(function(value){
// success
},function(error){
// failure
})
2、catch()
如果出现错误,抛出异常,不会停止运行,而是进入catch
方法中,所以catch
方法是用来捕获异常的。
3、all()
all
方法可以完成并行任务,它接受一个数组,数组的每一项都是一个promise
对象。当数组中所以的promise
的状态都达到resolved
的时候,all
方法的状态就会变成resolved
,如果有一个状态变成了rejected
,那么all
方法的状态就会变成rejected
。
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},2000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},1000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
console.log(res);
//结果为:[1,2,3]
})
注意:调用all
方法时的结果成功也是一个数组,这个数组按顺序保存每一个promise
对象resolve
执行时的值。
4、race()
race
方法与all
一样,接受的参数是一个每项都是promise
的数组,但是与all
不同的是,当最先执行完的事件执行完之后,就直接返回该promise
对象的值。如果第一个promise
对象状态变成了resolved
,那自身的状态变成了resolved
;反之第一个promise
变成rejected
,那自身状态就会变成rejected
。
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(1);
},2000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},1000) // 它是第一执行完的,输出这一个
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
console.log(res);
//结果:2
},rej=>{
console.log(rej)};
)
5、finally()
finally
方法用于指定不管Promise
对象最状态如何,都会执行的操作。该方法时ES8引入标准的。
(1)Promise.all
可以将多个Promise
实例包装成应该新的Promise
实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject
失败状态的值。传入的promise
对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的。
(2)Promise.race
就是赛跑的意思,意思就是说,Promise.race[(p1,p2,p3)]
里面哪个结果获得快,就返回哪个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决。
async/await
其实是Genterator
的语法糖,它是为了优化then
链而开发出来的。
async
函数返回一个Promise
对象,await
可以用于等待一个async
函数的返回值。如果说async
函数没有返回值,它会返回Promise.resolve(undefined)
。
await
表达式的运算结果取决于它等的是什么。
1、 如果它等到的不是一个Promise
对象,那么await
表达式的运算结果就是它等到的东西;
2、如果它等到的是一个Promise
对象,await
就忙碌起来了,它会阻塞后面的代码,等着Promise
对象,然后得到resolve
的值,作为await
表达式的运算结果。
优点:
async/await
传递中间值同步写法,非常优雅;async/await
可以用成熟的try/catch
;回调函数就是写处理逻辑的函数。
回调函数有一个致命的弱点,就是容易写出回调地狱。
回调地狱的根本问题就是:
1、嵌套函数存在耦合性,一旦有所改变,就会牵一发而动全身;
2、嵌套函数一多,就很难处理错误。
缺点:不能使用try/catch
捕获错误,不能直接return
。
解决回调地狱:Promise
、async/await
.
JS是单线程执行的,如果前面的代码影响了性能,就会导致setTimout
不会按期执行。setInterval
用法与setTimeout
基本一致,只是该函数是每隔一段时间执行一次回调函数。
通常来说不建议使用setInterval
:
1、它和setTimeout
一样,不能保证在预期的时间执行任务;
2、它存在执行累积问题。
如果有循环定时器的需求,其实完全可以通过requestAnimationFrame
来实现:
requestAnimationFrame
是H5的新特性,请求动画帧。官方解释:帧动画,就是可以一帧一帧的执行动画。
优势:
1、requestAnimationFrame
会把每一帧中所有DOM操作集中起来,在一次重绘或回流中完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
2、requestAnimationFrame
将不会进行重绘或回流,这当然就意味着更少的CPU,内存的使用。
1、扩展运算符
2、构造函数新增的方法
Array.from() // 将两类对象转为真正的数组
Array.of() // 将一组值转换为数组
3、实例对象新增的方法
copywithin(target,start,end) // 将指定位置的成员复制到其他位置(会覆盖原有成员)
find() // 用于找出第一个符合条件的数组成员
findIndex() // 返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
fill() // 填充一个数组
keys() // 是对键名的遍历
values() // 对键值的遍历
entries() // 是对键值对的遍历
includes() // 用于判断数组是否包含给定的值
flat() // 数组扁平化
flatMap() // 对原数组的每一个成员执行一个函数相当于执行Array.prototype.map(),然后对返回值组成的数组执行flat()方法,该方法返回一个新数组,不改变原数组
4、数组的空位
数组的空位指,数组的某一个位置没有任何值,ES6则是明确将空位转为undefined
5、排序的稳定性,将sort()
默认设置为最稳定的排序算法
1、属性的简写
ES6中,当对象键名与对应值名相等的时候,可以进行简写
const baz = {foo:foo}
// 等同于
const baz = {foo}
2、属性名表达式
ES6允许字面量定义对象时,将表达式放在括号内
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
3、super关键字
4、扩展运算符
5、属性的遍历
6、对象新增的方法
Object.is() 严格判断两个值是否相等
Object.assign() 用于对象的合并,浅拷贝
Object.setPrototypeOf() 设置一个对象的原型对象
Object.getPrototypeOf() 用于读取一个对象的原型对象
Object.keys() 返回自身的(不含继承的)所有可遍历属性的键名的数组
Object.values() 返回自身的所有可遍历属性的键对应值得数组
Object.entries() 返回一个对象自身得所有可遍历属性的键值对的数组
Object.fromEntries() 用于将一个键值对数组转为对象
1、ES6允许函数的参数设置默认值
function log(x, y = 'World') {
console.log(x, y);
}
console.log('Hello') // Hello World
console.log('Hello', 'China') // Hello China
console.log('Hello', '') // Hello
2、属性
(1)length
返回指定默认值的参数个数
(function (a) {}).length // 1
(function (a = 5) {}).length // 0 a被指定了值,不是默认值
(function (a, b, c = 5) {}).length // 2
(2)name
返回函数名
var f = function () {};
// ES6
f.name // "f"
如果将一个具名函数赋值给一个变量,则name属性返回这个具名函数原本的名字
const bar = function baz() {};
bar.name // "baz"
Function构造函数返回的函数实例,name属性的值为anonymous
(new Function).name // "anonymous"
bind返回的函数,name属性值会加上bound前缀
function foo() {};
foo.bind({}).name // "bound foo"
3、严格模式
4、箭头函数
一、什么是Proxy
1.Proxy
对象是ES6
新出的一个特性,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查询、赋值、枚举、函数调用等)
2.需要知道的是Vue2
中的双向绑定原理(数据劫持)采用的是Object.definePrototype()
,而在Vue3中数据劫持原理采用的Proxy
代理。
二、用法
Proxy
为构造函数,用来生成Proxy
实例
var proxy = new Proxy(target, handler)
// target 表示所要拦截的目标对象
// handler 通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理P的行为
get
方法
get
方法用于拦截某个属性的读取操作,可以接受二个参数,分别为目标对象,属性名。
let obj = { name: "jack", age: "20" };
// 给obj设置一个代理
let p = new Proxy(obj, {
get(target, property) {
console.log("我拦截了" + target + "读取" + property);
console.log("它的值为" + target[property]);
// 定义你要返回的值
return target[property];
},
});
//读取obj的age属性看看,注意定义代理后得用代理来调用属性或方法
console.log(p.age);
set
方法
set
方法用来拦截某个属性的赋值操作,三个参数依次为目标对象、属性名、属性值。
let obj = { name: "jack", age: "20" };
let p = new Proxy(obj, {
set(target, property, value) {
console.log("要设置对象属性?我拦截到了~");
console.log("要修改成" + value + "?");
console.log("我就不给你改,我改成666");
target[property] = 666;
},
get(target, property) {
return target[property];
},
});
//修改obj.age的值为30;
p.age = 30;
//读取obj的age属性看看,注意定义代理后得用代理来调用属性或方法
console.log(p.age);
取消代理:Proxy.revocable(target,handler)
三、使用场景
Proxy
其功能非常类似于设计模式中的代理模式,常用功能如下:
1、拦截和监视外部对象的访问
2、降低函数或类的复杂度
3、在复杂操作前对操作进行校验或所对需要资源进行管理
模块(Module
),是能够单独命名并独立地完成一定功能的程序语句的集合。
什么需要模块化?
1、代码抽象
2、代码封装
3、代码复用
4、依赖管理
如果没有模块化,我们的代码会怎么样?
1、变量和方法不容易维护,容易污染全局作用域
2、加载资源的方式通过script标签从上到下
3、 代码较多就会复杂
4、大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人奔溃
因此需要一种将JS程序模块化的机制,如
CommonJS
(典型代表:node.js
)
AMD
(典型代表:require.js
)
CMD
(典型代表:sea.js
)
ES6 在语言标准的以及层面上,实现了Module,即模块功能,完全可以取代 CommonJS和 AMD规范,成为浏览器和服务器通用的模块解决方案
因为
CommonJS
和AMD
模块,都只能在运行时确定这些东西。比如,CommonJS
模块就是对象,输入时必须查找对象属性;而ES6
设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量
ES6模块内自动采用严格模式,模块功能主要由两个命令构成:
1、export
:一个模块就是一个独立的文件,导出文件,外部能够读取模块内部的某个变量。
2、import
:引入模块使用,import
后面我们常接着from
关键字,from
指定模块文件的位置,可以是相对路径,也可以是绝对路径。
使用场景:如今,ES6模块化已经深入我们日常项目开发中,像Vue
,React
项目搭建项目,组件化开发。
Decorator
,即装饰器,是一个普通的函数,用于扩展类属性和类方法。
Decorator
两大优点:
1、代码可读性变强了,装饰器命名相当于一个注释
2、再不改变原有代码情况下,对原来功能进行扩展
用法:
Decorator
修饰对象为下面两种:
1、类的装饰,接受一个参数即类本身
2、类属性的装饰,接受三个参数,类的原型对象,需要装饰的属性名,装饰属性名的描述对象
== 注意 == :装饰器不能用于修饰函数,因为函数存在变量声明情况
{}的valueOf结果为{},toString的结果为“[object object]”
[]的valueOf结果为[],toString的结果为 ""(空的字符)
require.js
的核心原理是通过动态创建 script
脚本来异步引入模块,然后对每个脚本的 load
事件进行监听,如果每个脚本都加载完成了,再调用回调函数。
function myFunc() {
let a = b = 0;
}
myFunc();
上面代码变成了这样:
function myFunc() {
let a = (b = 0);
}
myFunc();
首先,表达式b = 0求值,在本例中b没有声明。因此,JS引擎在这个函数外创建了一个全局变量b,之后表达式b = 0的返回值为0,并赋给新的局部变量a。
Iterator
(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator
接口,就可以完成遍历操作。
语法:
let arr = [{num:1},2,3]
let it = arr[Symbol.iterator]() // 获取数组中的迭代器
console.log(it.next()) // { value: Object { num: 1 }, done: false }
console.log(it.next()) // { value: 2, done: false }
console.log(it.next()) // { value: 3, done: false }
console.log(it.next()) // { value: undefined, done: true }
[Symbol.iterator]
属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。
遍历过程:
1、创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
2、第一次调用指针对象的next
方法,可以将指针指向数据结构的第一个成员。
3、第二次调用指针对象的next
方法,指针就指向数据结构的第二个成员。
4、不断调用指针对象的next
方法,直到它指向数据结构的结束位置。
value
代表想要获取的数据;
done
布尔值,false
表示当前指针指向的数据有值,true
表示遍历已经结束。
作用:
1、为各种数据结构,提供一个统一的、简便的访问接口;
2、使得数据结构的成员能够按某种次序排列;
3、Iterator 接口主要供for…of
消费。
Generator
(生成器),最大的特点就是可以交出函数的执行权。我们可以通过yield
关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而异步编程提供解决方案。
function *generatorFn() {
console.log("a");
yield '1';
console.log("b");
yield '2';
console.log("c");
return '3';
}
let it = generatorFn()
it.next()
// a
it.next()
// b
it.next()
// c
it.next()
代码分析:
1、* 用来表示函数为Generator函数,写法有很多 function* fn()、function*fn()、function *fn()
2、函数内部有yield字段
yield 用来定义函数内部的状态,并让出执行权;
这个关键字只能出现在生成器函数体内,但是生成器中也可以没有 yield 关键字,函数遇到 yield 的时候会暂停,并把 yield 后面的表达式结果抛出去
3、调用后其函数返回值使用了next方法
调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可;
Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针;
所以要调用迭代器对象 Iterator 的 next方法,指针就会从函数头部或者上一次停下来的地方开始执行;
next 方法其实就是将代码的控制权交还给生成器函数。
理解yield
yield 表达式*
yield
命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器
function* gen1(){
yield ["a", "b", "c"]
}
for(let val of gen1()){
console.log(a)
}
// ["a", "b", "c"]
// ------------------- 上下分割
function* gen2(){
yield* ["a", "b", "c"]
}
for(let val of gen2()){
console.log(a)
}
// a b c
如有书写错误,可以留言告诉我,加油!