# 一、祖传开头:闭包
要理解闭包,首先要理解变量的作用域,在JavaScript中,变量的作用域分为两种,全局作用域以及局部作用域。JavaScript语言的特别之处就在于,函数内部可以访问函数外部的全局变量,但是函数外部无法读取函数内部的局部变量。
# 1.1 什么是闭包?
闭包是有权限访问其它函数作用域内的变量的一个函数。
在js中,变量分为全局变量和局部变量,局部变量的作用域属于函数作用域,在函数执行完以后作用域就会被销毁,内存也会被回收,但是由于闭包是建立在函数内部的子函数,由于其可访问上级作用域的原因,即使上级函数执行完,作用域也不会被销毁,此时的子函数——也就是闭包,便拥有了访问上级作用域中变量的权限,即使上级函数执行完以后作用域内的值也不会被销毁。
# 1.2 闭包解决了什么问题?
本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。由于闭包可以缓存上级作用域,这样函数外部就可以访问到函数内部的变量。
# 1.3 闭包产生的新问题
- 内存泄漏
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
另外,关于垃圾回收的问题,《JS 高级程序设计》里面有比较清晰的介绍,并不是闭包会导致内存泄漏,就像我们不能说菜刀会杀人一样。所以建议大家把 GC 也搞清楚,为什么闭包有时候可能导致内存泄漏,怎么处理?哪些浏览器会发生哪些不会发生,这样自己写起代码来也更容易操作。毕竟,所有的 JS 都包含闭包。
- this的问题
this对象是在运行时基于函数的执行环境绑定的,在全局函数中,this等于window,而当函数作为某个对象的方法调用时,this等于那个对象。不过匿名函数的执行环境具有全局性,因此其this对象通常指向window。但有时候,由于编写闭包的方式不同,这一点可能不会那么明显。也就是说使用闭包,有可能会意外改变this的值。
所以在实际场景中,我们一定要谨慎使用闭包。
# 1.4 闭包的应用场景
- 命名空间
- 变量私有化,如需操作则开放getter和setter方法
# 二、原型和原型链
- 所有的引用类型都有一个_proto_(隐式原型)属性,属性值是一个普通的对象
- 所有的函数除了有_proto_属性,还都有一个prototype(显式原型)属性,属性值是一个普通的对象
- 所有引用类型都有一个constructor(构造函数)属性,该属性(是一个指针)指向它的构造函数
- 所有引用类型的_proto_属性指向它构造函数的prototype
当一个对象调用自身不存在的属性/方法时,会先去它的_proto_上查找,也就是它的构造函数的prototype;如果没有找到,就会去该构造函数的prototype的proto指向的上一级函数的prototype中查找,最后指向null。这样一层一层向上查找的关系会形成一个链式结构,称为原型链。
用自己的方式(图)理解constructor、prototype、proto和原型链
# 三、原生AJAX请求步骤
五步使用法: (1).创建XMLHTTPRequest对象 (2).使用open方法设置和服务器的交互信息 (3).设置发送的数据,开始和服务器端交互 (4).注册事件 (5).更新界面
Get请求:
// 第一步:创建异步对象
let xhr = new XMLHttpRequest()
// 第二步:设置请求的url参数,参数1是请求的类型,参数2是请求的url,可以携带参数
xhr.open('get', '/baidu.com?username=1')
// 第三步:设置发送的数据,开始和服务端交互
xhr.send()
// 第四步:注册事件onreadystatechange,当状态改变时会调用
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
// 第五步:如果到达这一步,说明数据返回,请求的页面是存在的
console.log(xhr.responseText)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
Post请求:
// 第一步:创建异步对象
let xhr = new XMLHttpRequest()
// post请求一定要添加请求头,不然会报错
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
// 第二步:设置请求的url参数,参数1是请求的类型,参数2是请求的url,可以携带参数
xhr.open('post', '/baidu.com')
// 第三步:设置发送的数据,开始和服务端交互
xhr.send('username=1&password=123')
// 第四步:注册事件onreadystatechange,当状态改变时会调用
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
// 第五步:如果到达这一步,说明数据返回,请求的页面是存在的
console.log(xhr.responseText)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 四、关于事件委托
# 4.1 什么是事件委托?
事件委托也叫事件代理,就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
# 4.2 事件委托有什么作用?
- 提高性能:每一个函数都会占用内存空间,只需添加一个时间处理程序代理所有事件,所占用的内存空间更少
- 动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以具有和其它元素一样的事件。
# 4.3 怎么实现事件委托?
HTML代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul id="wrap">
<li class="item">按钮</li>
<li class="item">按钮</li>
<li class="item">按钮</li>
<li class="item">按钮</li>
</ul>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
首先来看一下不使用事件委托将怎么为他们都绑定上监听函数
<script>
window.onload = function () {
let lis = document.getElementsByClassName('item')
for (let i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
console.log('用力的点我')
}
}
}
</script>
2
3
4
5
6
7
8
9
10
不使用事件委托,那就要遍历每一个li元素,给每个li元素绑定一个点击事件,这样的做法非常耗费内存,如果有100个、1000个li元素,那对性能的影响是非常大的。
那么使用事件委托是怎么实现的呢?
第一种:原生写法
<script>
window.onload = function () {
let ul = document.getElementById('wrap')
ul.onclick = function (ev) {
// 获取到事件对象
let e = ev || window.event
// 如果点击的元素的calssName为item
if (e.target.className === 'item') {
console.log('用力的点我')
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
这样一来,通过事件委托,只需要在li元素的父元素ul上绑定一个点击事件,通过事件冒泡的机制,就可以实现li的点击效果。并且通过js动态添加li元素,也能绑定点击事件。
第二种:jQuery 的delegate 方法(推荐)
$(function(){
$("#wrap").delegate("li","click",function(event){
var target = $(event.target);
target.css("background-color","red");
})
})
2
3
4
5
6
第三种:jQuery 的on/bind方法
$(function(){
$("#wrap").on("click","li",function(event){
var target = $(event.target);
target.css("background-color","red");
})
})
2
3
4
5
6
相比于上面的delegate 方法,仅仅是将节点与事件调换个位置而已
bind的方法跟on一模一样
# 五、typeof null === object
null不是一个对象,但为什么typeof null === object
原理是这样的,不同的对象在底层都会表示为二进制,在js中如果二进制的前三位都为0,就会被判断为object类型,null的二进制全为0,自然前三位也是0,所以typeof null === object。
# 六、关于深拷贝和浅拷贝
**浅拷贝:**只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,A改变B也会跟着改变
**深拷贝:**深拷贝后,不仅表现出来的现象跟原对象一模一样。而且他们指向的是两块完全不同的内存地址,A和B互相不影响
# 6.1 实现浅拷贝
方法1:直接用=赋值
let obj1 = {a: 1}
let obj2 = obj1
2
方法2:合并方法Object.assign
let obj1 = {a: 1}
let obj2 = {}
Object.assign(obj2, obj1)
2
3
方法3:多层对象for in循环只遍历第一层
function shallowObj(obj) {
let result = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = obj[key]
}
}
return result
}
let obj1 = {
a: 1,
b: {
c: 2
}
}
let obj2 = shallowObj(obj1)
obj1.b.c = 3
console.log(obj2.b.c) // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6.2 实现深拷贝
方法1:用 JSON.stringify 把对象转换成字符串,再用 JSON.parse 把字符串转换成新的对象
let obj1 = {
a: 1,
b: 2,
}
let obj2 = JSON.parse(JSON.stringify(obj1))
2
3
4
5
方法2:采用递归去拷贝所有层级属性
function deepClone(obj) {
// 如果传入的值不是一个对象,就不执行
if (Object.prototype.toString.call(obj) !== '[object Object]') return
// 根据传入值的类型初始化返回结果
let result = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 如果obj是个对象,就递归调用deepClone去遍历obj的每一层属性,如果不是对象就直接返回值
result[key] = Object.prototype.toString.call(obj[key]) === '[object Object]' ? deepClone(obj[key]) : obj[key]
}
}
return result
}
// 改进判断对象的方法
console.log(typeof null === 'object') // true
console.log(Object.prototype.toString.call(null) === '[object Object]') // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
方法3:使用lodash 函数库实现深拷贝
let obj1 = {
a: 1,
b: 2,
}
let obj2 = _.cloneDeep(obj1)
2
3
4
5
方法4:通过jQuery的extend方法实现深拷贝
let array = [1,2,3,4]
let newArray = $.extend(true,[],array) // true为深拷贝,false为浅拷贝
2
方法5:用slice实现对数组的深拷贝
let arr1 = ["1","2","3"]
let arr2 = arr1.slice(0)
arr2[1] = "9"
console.log(arr2) // ['1', '9', '3']
console.log(arr1) // ['1', '2', '3']
2
3
4
5
方法6:使用es6 扩展运算符实现深拷贝
let obj1 = {brand: "BMW", price: "380000", length: "5米"}
let obj2 = { ...car, price: "500000" }
2
参考链接:js浅拷贝与深拷贝的区别和实现方式
# 七、阻止事件冒泡和默认事件
标准的DOM对象中可以使用事件对象的stopPropagation()方法来阻止事件冒泡,但在IE8以下中的事件对象通过设置事件对象的cancelBubble属性为true来阻止冒泡
默认事件通过事件对象的preventDefault()方法来阻止,而IE通过设置事件对象的returnValue属性为false来阻止默认事件
# 八、call()、apply()、bind()的区别
共同点:call()、apply()、bind()的作用都是用来改变this的指向的
# 8.1 call和apply的区别
传参形式不一样,call是逐个传参,apply直接传入数组
call():
Function.call(obj, param1,param2,param3)
接收到的是param1,param2,param3三个参数
apply():
Function.apply(obj, [param1,param2,param3])
接收到的是param1,param2,param3三个参数
2
3
4
5
6
7
call性能比apply好那么一些(尤其是传递给函数的参数超过3个的时候),所以开发时可以用call好一点。
# 8.2 bind是怎么用的?
bind(): const newFn = Funtion.bind(obj, param1,param2)
返回值是一个函数,需要()来调用
newFn(param3,param4)
接收到的是param1,param2,param3,param4四个参数
2
3
4