# JavaScript
# ajax
- 创建一个xmlhttprequest实例
- 发出http请求
- 接收服务器传回来的数据
- 更新网页上面的数据
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
// 0 表示实例方法已经生成了,open方法未调用
// 1 open已调用, 但是send未调用
// 2 send已调用, 并且返回了头信息和状态码, 但是未返回数据体
// 3 已开始接收数据体, 但未完全接收
// 4 已经完整的接收了数据体
if(xhr.readystate === 4) {
if(xhr.status === 200) {
// 200 服务器正常
// 301 永久移动
// 302 暂时移动
// 304 未修改
// 307 暂时重定向
// 401 未授权
// 403 禁止访问
// 404 未发现指定网址
// 500 服务器错误
console.log(xhr.responseText)
} else {
console.log(xhr.statusText)
}
}
};
xhr.onerror = function() {
console.log(xhr.statusText)
};
xhr.open('GET',url,true);
xhr.send(null);
# JavaScript执行机制
# JavaScript是按照语句出现的顺序执行的
let a = "1";
console.log(a);
let b = "2";
console.log(b);
// "1"
// "2"
# 案例问题
setTimeout(() => {
console.log("定时器开始啦");
})
new Promise(function(resolve) {
console.log("马上执行for循环");
for(var i = 0; i < 10000; i++) {
i == 99 && resolve();
}
}).then(() => {
console.log("执行then")
})
console.log("执行代码结束");
# JavaScript Event Loop(事件循环)
JavaScript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!
既然js是单线程,那就是一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行,如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?所有我们将任务分为两种:
- 同步任务
- 异步任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久,就是异步任务。
看图片:
导图要表达的内容用文字来表述的话:
- 同步和异步任务分别进入不同的执行“场所”,同步进入主线程,异步的进入Event Table并注册函数;
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue.
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行;
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)
那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
let data = [];
$.ajax({
url: "xxx",
methods: "GET",
data: data,
success: (res) => {
console.log("发送成功");
}
})
console.log("代码执行结束");
简单的ajax请求代码:
- ajax 进入 Event Table,注册success函数;
- 执行console.log("代码执行结束");
- ajax事件完成后,回调函数success进入 Event Queue;
- 主线程从Event Queue读取回调函数 success 并执行;
# setTimeout
setTimeout
无需再多言,大家对他的第一印象就是异步可以延时执行,我们经常这么实现延时3秒执行:
setTimeout(() => {
console.log("延迟3秒");
},3000)
渐渐的setTimeout用的地方多了,问题也出现了,有时候明明写的延时3秒,实际却5,6秒才执行函数;
setTimeout(() => {
task()
},3000)
sleep(10000000)
但我们把这段代码在chrome执行一下,却发现控制台执行task()需要的时间远远超过3秒,说好的延时三秒,为啥现在需要这么长时间啊?
这时候我们需要重新理解setTimeout的定义。我们先说上述代码是怎么执行的:
task()
进入Event Table并注册,计时开始;- 执行
sleep
函数,很慢,非常慢,计时仍在继续。 - 3秒到了,计时事件timeout完成,task()进入Event Queue,但
sleep
太慢了,还没有执行完,必须得等着; - sleep执行完了,
task()
终于从Event Queue进入了主线程执行;
上述流程得知:我们知道setTimeout
这个函数,是经过指定时间后,把要执行的任务加到Event Queue中,又因为是单线程任务一个一个执行,如果前面的任务需要时间太久,那么只能等着,导致真正的延迟时间远远大于3秒;
我们还经常需要setTimeout(fn,0)
这样的代码,0秒后执行又是什么意思?是不是可以立即执行了?
No! 不会的,setTimeout(fn,0)
的含义是,指某个任务在主线程最早可得的空闲时间执行,意思就是不用在等多少秒了,只要主线程执行内的同步任务全部执行完成,栈为空就马上执行。
//代码1
console.log('先执行这里');
setTimeout(() => {
console.log('执行啦')
},0);
//先执行这里
//执行啦
//代码2
console.log('先执行这里');
setTimeout(() => {
console.log('执行啦')
},3000);
//先执行这里
// ... 3s later
// 执行啦
关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒;
# setInterval
上面说完了setTimeout,当然不能错过它的孪生兄弟setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于**setInterval(fn,ms)**来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。
# Promise 与 process.nextTick(callback)
传统的定时器我们已经研究过了,接着我们探究Promise与process.nextTick(callback)的表现。
除了广义的同步任务和异步任务,我们对任务有更精细的定义:
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval;
- micro-task(微任务):Promise,process.nextTick;
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。
先执行微任务 后执行宏任务
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
setTimeout(() => {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 这段代码作为宏任务,进入主线程
- 先遇到setTimeout,将回调函数注册之后分发到宏任务Event Queue;
- 接下来遇到Promise,new Promise 立即执行,then()函数分发到微任务Event Queue;
- 遇到console.log()立即执行;
- 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
- 第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
事件循环,宏任务,微任务的关系如图所示:
分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
- 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
- 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。
- 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
- 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
- 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。
- 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
- 我们发现了process1和then1两个微任务。
- 执行process1,输出6。
- 执行then1,输出8。
好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:
- 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
- 第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。
- 输出3。
- 输出5。
- 第二轮事件循环结束,第二轮输出2,4,3,5。
- 第三轮事件循环开始,此时只剩setTimeout2了,执行。
- 直接输出9。
- 将process.nextTick()分发到微任务Event Queue中。记为process3。
- 直接执行new Promise,输出11。
- 将then分发到微任务Event Queue中,记为then3。
- 第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。
- 输出10。
- 输出12。
- 第三轮事件循环结束,第三轮输出9,11,10,12。
# 结尾
# js的异步
我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。
# 事件循环Event Loop
事件循环是js实现异步的一种方法,也是js的执行机制。
# javascript的执行和运行
执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。
# setImmediate
微任务和宏任务还有很多种类,比如setImmediate等等,执行都是有共同点的,有兴趣的同学可以自行了解。
# 最后的最后
- javascript是一门单线程语言
- Event Loop是javascript的执行机制
- 牢牢把握两个基本点
# js 滚动底部
判断内容滚动到底部,需要知道的信息;
- 1、内容区域的真实(也就是滚动区域)scrollHeight;
- 2、滚动条距离顶部的位置 scrollTop;
- 3、内容区域的可见高度 clientHeight;
- 4、判断元素是否滚动到底部 e.clientHeight + e.scrollTop = scrollHeight;
兼容写法
// 获取scrollTop
function getScrollTop () {
var scrollTop = 0,bodyScrollTop = 0, documentScrollTop = 0;
if(document.body) {
bodyScrollTop = document.body.scrollTop;
}
if(document.documentElement) {
documentScrollTop = document.documentElement.scrollTop;
}
scrollTop = (bodyScrollTop - documentScrollTop > 0) ? bodyScrollTop : documentScrollTop;
return scrollTop;
}
# 文档的总高度
function getScrollHeight() {
let scrollHeight = 0,bodyScrollHeight = 0,documentScrollHeight = 0;
if(document.body) {
bodyScrollHeight = document.body.scrollHeight;
}
if(documentScrollHeight) {
documentScrollHeight = document.documentElement.scrollHeight;
}
scrollHeight = (bodyScrollHeight - documentScrollHeight > 0) ? bodyScrollHeight :
documentScrollHeight;
return scrollHeight;
}
# 内容区域可见高度
function getWindowHeight() {
let windowHeight = 0;
if(document.compatMode == "CSS1Compat") {
windowHeight = document.documentElement.clientHeight;
} else {
windowHeight = document.body.clientHeight;
}
return windowHeight;
}
# 回到底部
dom.addeventlistener('click',function() {
window.scrollTo(0,document.documentElement.scrollHeight);
})
# 回到顶部
// 定时器又有 setInterval、setTimeout 和 requestAnimationFrme 这三种使用
// 下面使用性能最好的定时器requestAnimationFrame来实现
let timer = null;
btn.addEventlistener('click',function() {
cancelAnimationFrame(timer);
timer = requestAnimationFrame(function fn() {
let oTop = document.body.scrollTop || document.documentElement.scrollTop;
if(oTop > 0) {
/* 三种方法 */
document.body.scrollTop = document.documentElement.scrollTop = oTop - 50;
// window.scrollTo(0,oTop - 50)
// window.scrollBy(0,-50)
timer = requestAnimationFrame(fn);
} else {
cancelAnimationFrame(timer)
}
})
})
# 判断是否底部
window.onscroll = function () {
if(Math.ceil(getScrollTop) + getWindowHeight() === getScrollHeight()) {
console.log('到底了')
}
}
# 懒加载与预加载
# 懒加载
# 1.1 什么懒加载
懒加载就是延迟加载
当访问一个页面的时候,先把img元素或是其他元素的背景图片路径替换成一张大小为1*1px图片的路径(这样就只需请求一次,"占位图"), 只有当图片出现在浏览器的可视区域内时,才设置图片真正的路径,让图片显示出来
# 1.2 为什么使用懒加载
很多页面,内容很丰富,页面很长,图片较多。比如说各种商场页面,这些页面图片数量多,而且比较大,要是页面载入就一次性加载完毕,会花费很多时间
# 1.3 懒加载原理
页面中的img元素,如果没有src属性,浏览器就不会发出请求去下载图片,只有通过JavaScript设置了图片路径,浏览器才会发送请求
懒加载的原理就是先在页面中把所有的图片统一使用一张占位图进行占位,把真正的路径存在元素的 data-url
(这个名字起个自己认识好记的就行了)属性中,要用的时候就取出来,再设置
# 1.4 懒加载实现步骤
- 首先,不要将图片地址放在src属性中,而是放在其他属性(data-origin)中
- 页面加载完成后,根据scrollTop判断图片是否在用户的视野内,如果在,则将data-original属性中的值取出来存放在src属性中
- 在滚动事件中重复判断图片是否进入视野,如果进入,则将data-origin属性的值取出来存放在src属性中
# 1.5 懒加载的优点
页面加载速度快,可以减轻服务器的压力,节约了流量,用户体验好
# 1.6 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style type="text/css" media="all">
body,
ul,
li {
padding: 0;
margin: 0;
}
body {
font-size: 12px;
-webkit-user-select: none;
-webkit-text-size-adjust: none;
font-family: helvetica;
}
.lazyLoad ul li {
height: 200px;
border: 1px solid red;
margin-bottom: 2px;
overflow: hidden;
text-align: center;
}
</style>
</head>
<body>
<div class="lazyLoad">
<ul>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
<li><img data-src="http://img0.imgtn.bdimg.com/it/u=2873269578,797009742&fm=26&gp=0.jpg" alt=""></li>
</ul>
</div>
<script>
function lazyLoad(obj, option) {
this.init.apply(this, arguments)
}
lazyLoad.prototype = {
init: function (el, param) {
let opt = {
dataSrc: 'lazyImg',
dataImg: './11.jpg', // 占位图
contens: typeof el === 'string' ? this.getElement(el) : el || document.body, // 判断 el 传进来的是 dom对象 还是 string
cb: null
}
if (param && param instanceof Object) {
for (let key in param) {
if (param.hasOwnProperty(key)) {
opt[key] = param[key]
}
}
}
this.opt = opt
console.log(this.opt)
this.render()
this.bindUI()
},
render: function () {
let _opt = this.opt,
_imgArr = [],
_dom = _opt.contens,
// 获取到传进来的dom对象 里面所有的 img 元素
_img = _dom.querySelectorAll('['+ _opt.dataSrc +']');
if(_img.length) {
let _tagName
Array.prototype.forEach.call(_img,function(item,i) {
console.log(item.tagName);
_tagName = item.tagName.toLowerCase() // 将 元素 的 tagName 小写化
if(_tagName === 'img' && !item.getAttribute('src')) {
item.setAttribute('src',this.opt.dataImg); // 将img元素的src的值 设为 传进来的 占位图
}
_imgArr.push({
obj: item,
tagName: item.tagName.toLowerCase(),
src: item.getAttribute(_opt.dataSrc)
})
}.bind(this));
}
this.imgArr = _imgArr
this.loadImg()
},
bindUI: function() {
window.addEventListener('scroll',function() {
this.loadImg()
}.bind(this),false)
},
loadImg: function() {
let _imgArr = this.imgArr;
let _dataSrc , _src;
if(_imgArr.length) {
_imgArr.forEach(function(item,i) {
// console.log(item)
if(this.isLoad(item.obj)) { // 判断 该图片是否进入了 可视区域
// 如果是图片
if(item.tagName === 'img') {
_dataSrc = this.opt.dataSrc
_src = item.obj.getAttribute(_dataSrc)
console.log(_dataSrc);
console.log(_src); // img 下 data-src 属性下的值
if(_src) {
console.log(item)
item.obj.setAttribute('src',_src); // 将 data-src 的值 赋值给 img 下的 src 属性中
item.obj.removeAttribute(_dataSrc); // 删除 data-src 属性
if(typeof this.opt.cb === 'function') {
return this.opt.cb.call(this,item.obj)
}
}
} else {
// 加载文件
if(typeof this.opt.cb === 'function') {
if(_imgArr[i].isLoad) {
return false
}
}
_imgArr[i].isLoad = 1;
return this.opt.cb.call(this,item.obj)
}
}
}.bind(this))
}
},
isLoad: function(img) {
let _scrollTop = document.body.scrollTop || document.documentElement.scrollTop
let _winHeight = window.innerHeight
let _offsetTop = img.offsetTop
let _height = img.offsetHeight
if(_offsetTop + _height >= _scrollTop && _offsetTop <= _winHeight + _scrollTop) {
/**
_offsetTop + _height >= _scrollTop
img图片的距离可视区域的高度 + 图片的高度 大于等于 浏览器窗口顶部与文档顶部之间的距离 也就是滚动条滚动的距离
_offsetTop <= _winHeight + _scrollTop
img图片的距离可视区域的高度 小于等于 屏幕可视窗口大小 + 浏览器窗口顶部与文档顶部之间的距离 也就是滚动条滚动的距离
*/
return true
}
return false
},
getElement: function(o) {
return document.querySelector(o)
}
}
let loadPic = function (obj, option) {
if (!obj) return
return new lazyLoad(obj, option)
}
var t = loadPic('.lazyLoad',{
dataSrc : 'data-src',
cb : function(item){
// console.log(item)
}
})
</script>
</body>
</html>
# 预加载
# 1.1 什么是预加载
提前加载图片,当用户查看时可以直接从本地缓存中渲染
# 1.2 为什么要使用预加载
图片预先加载到浏览器中,访问者便可顺利地在你的网站上冲浪,并享受到极快的加载速度。这对图片画廊及图片占据很大比例的网站来说十分有利,它保证了图片快速、无缝地发布,也可帮助用户在浏览你网站内容时获得更好的用户体验。
# 2.3 实现预加载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#preload_01 img {
width: 100px;
height: 100px;
margin: 8px;
}
</style>
</head>
<body>
<div id="preload_01"></div>
<script>
function preload() {
let images = new Array()
for (let i = 0; i < preload.arguments.length; i++) {
images[i] = new Image()
images[i].src = preload.arguments[i]
document.getElementById('preload_01').appendChild(images[i]);
}
}
// 使用addLoadEvent()函数来延迟preloader()函数的加载时间,直到页面加载完毕。
function addLoadEvent(func) {
var oldonload = window.onload;
if (typeof window.onload != 'function') {
window.onload = func;
} else {
window.onload = function () {
if (oldonload) {
oldonload();
}
func();
}
}
}
addLoadEvent(preload(
"http://img4.imgtn.bdimg.com/it/u=1688026885,2773767715&fm=26&gp=0.jpg",
"http://img4.imgtn.bdimg.com/it/u=1688026885,2773767715&fm=26&gp=0.jpg",
"http://img4.imgtn.bdimg.com/it/u=1688026885,2773767715&fm=26&gp=0.jpg"
))
</script>
</body>
</html>
# 懒加载和预加载的对比
- 概念
- 懒加载也叫延迟加载:js图片延迟加载,延迟加载图片或符合某些条件时才加载某些图片
- 预加载:提前加载图片,当用户需要查看时可以直接从本地缓冲中渲染
- 区别:
- 两种技术的本质:两者行为是相反的,一个是提前加载,一个是迟缓甚至不加载
- 懒加载对服务器端有一定的缓解压力,预加载则会增加服务器端压力
- 意义:
- 懒加载的主要目的是作为服务器端的优化,减少请求或延迟请求数
- 预加载可以说是牺牲服务器前端性能,换取更好的用户体验,这样可以使用户的操作得到最快的反映。
# JavaScript 深拷贝与浅拷贝
# 简单问题
var name = "hanzo";
var a = name;
name = '111';
console.log(a); // hanzo
console.log(name); // 111
var b = {
name: 'genji',
age: 32
}
var c = b;
console.log(c); // {name: "genji", age: 32}
b.name = 'hanzo';
console.log(b) // {name: "hanzo", age: 32}
console.log(c); // {name: "hanzo", age: 32}
# js数据类型
# 1. 简单理解栈堆
在计算机领域中,栈堆是两种数据结构,它们只能在一端(称为栈顶(top))对数据项进行插入和删除
堆 队列优先, 先进先出, 由操作系统自动分配释放, 存放函数的参数值, 局部变量的值等。 其操作方式类似于数据结构中的栈。
栈 先进后出;动态分配的空间 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
# 2. js基本类型和引用类型与堆栈有什么联系?
# 我们都知道javascript中的基本数据类型有:string number boolean null undefined symbol, 引用类型有:Array Object
简单数据类型是存在栈内存中的
引用类型是保存在堆内存中的,然后再栈内存中保存一个对堆内存中实际对象的引用。所以,javascript中引用数据类型的操作都是操作对象的引用而不是实际的对象, 可以理解为:栈内存中保存了一个地址,这个地址和堆内存中的实际值是相关的。
var name = 'hanzo';
var age = 25;
var job;
var arr = [1,2,3];
var obj = {age: 25};
// 可以通过下图来表示数据类型在内存中的存储情况:
此时name
,age
,job
三种基本数据类型是直接存在栈内存中的,而arr,obj在栈内存中只是存了一个地址来表示对堆内存中的引用。
# 复制
# 基本数据类型
对于基本数据类型,如果进行复制,系统会自动为新的变量在栈内存中分配一个新值,很容易理解
# 引用数据类型
如果对于数组、对象这样的引用数据类型而言,复制的时候就会有所区别了: 系统也会自动为新的变量在栈内存中分配一个值,但这个值仅仅是一个地址。也就是说,复制出来的变量和原有的变量具有相同的地址值,指向堆内存中的同一个对象。
如果所示,执行了var objCopy=obj之后,obj和objCopy具有相同的地址值,执行堆内存中的同一个实际对象。
这有什么不同呢?
当我修改obj或objCopy时,都会引起另一个变量的改变。
# 为什么基础数据类型存在栈中,而引用数据类型存在堆中呢?
- 堆比栈大,栈比对速度快。
- 基础数据类型比较稳定,而且相对来说占用的内存小。
- 引用数据类型大小是动态的,而且是无限的。
- 堆内存是无序存储,可以根据引用直接获取。
# 深拷贝/浅拷贝
对于基本数据类型的拷贝,并没有深浅拷贝的区别,我们所说的深浅拷贝都是对于引用数据类型而言。
# 浅拷贝
浅拷贝的意思就是只复制引用,而未复制真正的值。
const originArray = [1,2,3,4,5];
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneArray = originArray;
const cloneObj = originObj;
console.log(cloneArray); // [1,2,3,4,5]
console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}}
cloneArray.push(6);
cloneObj.a = {aa:'aa'};
console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]
console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
// 上面的代码是最简单的利用 `=` 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 `cloneArray` 和 `cloneObj` 改变,`originArray` 和 `originObj` 也随着发生了变化。
# 深拷贝
深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。
# 1. 利用JSON.parse 和 JSON.stringify 方法
// 1.实现简单的深拷贝
const hanzo = [1,2,3,4];
const genji = JSON.parse(JSON.stringify(hanzo));
console.log(hanzo === genji); // false
hanzo.push(5);
genji.splice(0,1);
console.log(hanzo); // [1,2,3,4,5]
console.log(genji); // [2,3,4]
// 缺陷:只能实现一些简单的深拷贝。但是下面这种情况就不适合了。
// 2.
const obj = {
name: 'hanzo',
age: 38,
sayHello: function() {
console.log('半藏')
}
};
console.log(obj); // {name: "hanzo", age: 38, sayHello: ƒ}
const obj6 = JSON.parse(JSON.stringify(obj));
console.log(obj6); // {name: "hanzo", age: 38}
// sayHello 这个funciton 并没有没拷贝下来
// 原因:undefined、function、symbol 会在转换过程中被忽略,
// 所以如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝。
# 2. 递归
递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作,
function deepCopy(source) {
const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
for(let keys in source) {
if(source[keys] && typeof source[keys] === 'object') { // 如果值是对象,就递归一下
targetObj[keys] = source.constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
} else {
targetObj[keys] = source[keys];
}
}
return targetObj
}
const hanzo = {
name: 'hanzo',
age: 38,
sayHello: function() {
console.log('我是半藏')
}
}
const genji = deepCopy(hanzo);
genji.name = 'genji';
genji.age = 35;
genji.sayHello = function() {
console.log('我是源氏')
};
console.log(hanzo); // {name: "hanzo", age: 38, sayHello: ƒ}
console.log(genji); // {name: "genji", age: 35, sayHello: ƒ}
console.log(hanzo === genji); // false
hanzo.sayHello(); // 我是半藏
genji.sayHello(); // 我是源氏
# 3. javascript的其他拷贝方法
我们知道在 JavaScript 中,数组有两个方法 concat 和 slice 是可以实现对原数组的拷贝的,这两个方法都不会修改原数组,而是返回一个修改后的新数组。
同时,ES6 中 引入了 Object.assgn 方法和 ... 展开运算符也能实现对对象的拷贝。
那它们是浅拷贝还是深拷贝呢?
# 4. concat slice
const arr1 = [1,2,3,4,5];
const arr2 = arr1.concat();
arr1.push(6);
arr2.splice(0,1);
console.log(arr2 === arr1); // false;
console.log(arr1); // [1, 2, 3, 4, 5, 6]
console.log(arr2); // [2, 3, 4, 5]
// 看上去像深拷贝
// 但如果是多层呢?
const arr1 =[1,[1,2,3],{a:1}];
const arr2 = arr1.concat();
arr2[1].push(4);
arr2[2].a = 666;
console.log(arr1 === arr2); // false
console.log(arr1); // [1,[1,2,3,4],{a:666}];
console.log(arr2); // [1,[1,2,3,4],{a:666}];
arr1
中含有数组 [1,2,3]
和对象 {a:1}
,如果我们直接修改数组和对象,不会影响 arr1
,但是我们修改数组[1,2,3]
或对象 {a:1}
时,发现 arr1
也发生了变化。
结论:concat 只是对数组的第一层进行深拷贝。
slice 和 concat 一样 所以不举例
# Object.assign()
const hanzo = {
name: 'hanzo',
xage: 38,
children: [1,2,3,4],
sayHello() {
xconsole.log(666)
}
};
const genji = Object.assign({}, hanzo);
console.log(genji === hanzo); // false;
genji.children.push(666);
console.log(hanzo); // {name: "hanzo", age: 38, children: [1,2,3,4,666], sayHello: ƒ}
console.log(genji); // {name: "hanzo", age: 38, children: [1,2,3,4,666], sayHello: ƒ}
Object.assign() 也是首层浅拷贝
# ...展开运算符
const Array1 = [1,2,3,4,[5,6],7,8];
const Obj1 = {
a: 1,
b: {
bb: 233
}
};
const cloneArray = [...Array1];
cloneArray[4].push(666);
console.log(cloneArray === Array1); // false
console.log(cloneArray); // [1,2,3,4,[5,6,666],7,8];
console.log(Array1); // [1,2,3,4,[5,6,666],7,8];
const cloneObj = {...Obj1};
console.log(cloneObj === Obj1);
cloneObj.b.bb = 666;
console.log(cloneObj);// {a:1,b:{bb:666}}
console.log(Obj1); // {a:1,b:{bb:666}}
...展开运算符也是首层浅拷贝
# 首层浅拷贝
function shallowClone(source) {
xconst targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
for (let keys in source) { // 遍历目标
if (source.hasOwnProperty(keys)) {
targetObj[keys] = source[keys];
}
}
return targetObj;
}
# 总结
- 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
- javascript中数组和对象自带的拷贝方法都是“首层浅拷贝”;
- JSON.stringify 实现的深拷贝,但是对目标对象有要求;
- 若想真正意思上的深拷贝,请递归。