文将从以下六个方面讲解引用类型和基本类型
1. 概念
2. 内存图
3. 引用类型和基本类型作为函数的参数体现的区别
4. 引用类型的优点:
5. 引用类型的赋值(对比基本类型)
6. 浅拷贝和深拷贝
以下为详细内容:
1. 概念:
基本类型也叫简单类型,存储的数据是单一的,如:学生的个数就是一个数字而已;引用类型也叫复杂类型,存储的数据是复杂的,如:学生,包括学号,姓名,性别,年龄等等很多信息。从内存(大家如果不懂内存,请查阅相关资料)的角度来说:基本类型只占用一块内存区域;引用类型占用两块内存区域。即定义基本类型的变量时,在内存中只申请一块空间,变量的值直接存放在该空间;定义引用类型的变量时(容易理解的是,我门看到new运算符,一般就是定义引用类型的变量),在内存中申请两块空间,第一块空间存储的是第二块空间的地址,第二块空间存储的是真正的数据;第一块空间叫作第二块空间的引用(地址),所以叫作引用类型。
javaScript中的基本类型包括:数字(Number),字符串(String),布尔(Boolean),Null,Undefined五种;
javascript的引用类型是:Object。而Array,Date是属于Obejct类型。
2. 内存图:
如下代码(都是定义了两个局部变量):
以上两行代码的内存图:
可以看到,num变量只占用了一块内存区域;arr变量占用了两块内存区域,arr变量在栈区(不懂栈区的人,先不要想太多)申请了一块内存区域,存储着地址,存储的地址是堆区的地址。而堆区中真正才存储着数据,所以说,arr变量占用了两块内存区域。这样看来,引用类型的变量好像还占用内存多了。哈哈,不要着急,后面了解了引用类型的优点后,你就会觉得这是问题了。
当我们读取num变量的值时,直接就能读到,但是当我们要读取arr里的值时,先找到arr中的地址,然后根据地址再找到对应的数据。
引用类型,类似于windows操作系统中的快捷方式。快捷方式就是一个地址,真正的内容是快捷方式所指向的路径的内容。如:我们把d:\t.txt文件创建一个快捷方式放在桌面上,那么,桌面上的快捷方式会占用桌面的空间,而d:\t.txt会占用d盘的空间,所以,占用了两块空间。
基本类型就相当于文件。
引用类型,类似于我们在入学报名填写报名表时,填写家庭地址,这个家庭地址就相当于第一块空间,真正你家(第二块内存空间)不在报名表上。学校要找你家,先在报名表上找到你家的地址,然后根据地址,才能找到你家去。
3. 引用类型的优点:
引用类型作为函数的参数时,优点特别明显,第一,形参传递给实参时,只需要传递地址,而不需要搬动大量的数据(节约了内存开销);第二,形参对应的数据改变时,实参对应的数据也在改变(很多时候,我们希望这样)。
如以下代码:
先定义函数(冒泡排序)
当调用冒泡排序时,
看看内存以上代码执行时的,内存变化:
图中,当执行,①对应的代码(var arr1=[250,2,290,35,12,99];)时,内存中会产生①对应的变化,即在栈中申请一块内存区域,起名为arr1,在堆区中申请内存空间放置250,2,290,35,12,99,并把堆区中的内存的地址赋给arr1的内存中;当执行②对应的代码bubble(arr1)时,调用函数。这时候会定义形参arr(内存中③对应的变化),即在栈中申请一块内存区域,起名为arr,并把arr1保存的地址赋给了arr(内存中②表示的赋值),这样,形参arr和实参arr1就指向了同一块内存区域。数组中的值250,2,290,35,12,99在内存中只有一份。即,不用把数组中每个元素的值再复制一份,节约了内存。如果对内存图看懂了,那么,当形参arr对应的数据顺序改变了,实参arr1对应的数据顺序也就改变了。即,实现了形参数据改变时,实参数据也改变了。所以,bubble函数不需要返回值,依然可以达到排序的目的。可以运行我示例中的代码,看看是不是达到了排序的效果。
补充,基本类型作为函数参数的内存变化:
内存图:
4. 引用类型变量的赋值:
引用类型变量赋值时,赋的是地址。即两个引用类型变量里存储的是同一块地址,也就是说,两个引用类型变量都指向同一块内存区域。所以,两个引用类型变量对应的数据时一样的。
基本类型变量赋值时的内存变化。
5. 浅拷贝和深拷贝
先说对象的复制,上面说了,引用类型(对象)的赋值,只是赋的地址,那么要真正复制一份新的对象(即克隆)时,又该怎么办。
但是,当一个对象的属性又是一个引用类型时,会出现浅拷贝和深拷贝的问题。用一个自定义的object类型来说明问题。
如:
person2.name="张四"; //不会改变掉person1的name属性。
person2.address.country="北京";//会改变掉person1的address.country
大家注意看,person1和person2的name属性各有各的空间,但是person1.address.country和person2.address.country是同一块空间。所以,改变person2.address.country的值时,person1.address.country的值也会改变。这就说明拷贝(克隆)的不到位,这种拷贝叫作浅拷贝,而进一步把person1.address.country和person1.address.name也拷贝(克隆)了,就是深拷贝。要做到深拷贝,就需要对每个属性的类型进行判断,如果是引用类型,就再循环进行拷贝(需要用到递归)。
(未完待续)
千锋HTML5教学部 田江
avaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一但"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
复制代码
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。为了更好地理解Event Loop,请看下图
事件循环可以简单描述为:
函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空; 在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待; 当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步。
接下来看一个异步函数执行的例子:
var start=new Date();
setTimeout(function cb(){
console.log("时间间隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
复制代码
JS的异步有一个机制的,就是会分为宏任务和微任务。宏任务和微任务会放到不同的event queue中,先将所有的宏任务放到一个event queue(macro-task),再将微任务放到一个event queue(micro-task)中。执行完宏任务之后,就会先从微任务中取这个回调函数执行。
最开始, 执行栈为空, 微任务队列为空, 宏任务队列有一个 script 标签(内含整体代码)
将第一个宏任务出队, 这里即为上述的 script 标签
整体代码执行过程中, 如果是同步代码, 直接执行(函数执行的话会有入栈出栈操作), 如果是异步代码, 会根据任务类型推入不同的任务队列中(宏任务或微任务)
当执行栈执行完为空时, 会去处理微任务队列的任务, 将微任务队列的任务一个个推入调用栈执行完
微任务执行完后,检查是否需要重新渲染 UI。
...往返循环直到宏任务和微任务队列为空
出队一个宏任务 -> 调用栈为空后, 执行一队微任务 -> 更新界面渲染 -> 回到第一步
一个event loop有一个或者多个task队列。task任务源非常宽泛,比如ajax的onload,click事件,基本上我们经常绑定的各种事件都是task任务源,还有数据库操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任务源。总结来说task任务源:
microtask 队列和task 队列有些相似,都是先进先出的队列,由指定的任务源去提供任务,不同的是一个 event loop里只有一个microtask 队列。另外microtask执行时机和Macrotasks也有所差异
下图是一个事件循环的流程
举个简单的例子,假设一个script标签的代码如下:
Promise.resolve().then(function promise1 () {
console.log('promise1');
})
setTimeout(function setTimeout1 (){
console.log('setTimeout1')
Promise.resolve().then(function promise2 () {
console.log('promise2');
})
}, 0)
setTimeout(function setTimeout2 (){
console.log('setTimeout2')
}, 0)
script里的代码被列为一个task,放入task队列。
循环1:
循环2:
循环3:
console.log('-------start--------');
setTimeout(()=> {
console.log('setTimeout'); // 将回调代码放入另一个宏任务队列
}, 0);
new Promise((resolve, reject)=> {
for (let i=0; i < 5; i++) {
console.log(i);
}
resolve()
}).then(()=>{
console.log('Promise'); // 将回调代码放入微任务队列
})
console.log('-------end--------');
-------start--------
0
1
2
3
4
-------end--------
Promise
setTimeout
由EXP1,我们可以看出,当JS执行完主线程上的代码,会去检查在主线程上创建的微任务队列,执行完微任务队列之后才会执行宏任务队列上的代码
主线程=> 主线程上创建的微任务=> 主线程上创建的宏任务
script里的代码被列为一个task,放入task队列。
循环1:
循环2:
setTimeout(_=> console.log('setTimeout4'))
new Promise(resolve=> {
resolve()
console.log('Promise1')
}).then(_=> {
console.log('Promise3')
Promise.resolve().then(_=> {
console.log('before timeout')
}).then(_=> {
Promise.resolve().then(_=> {
console.log('also before timeout')
})
})
})
console.log(2)
Promise1
2
Promise3
before timeout
also before timeout
setTimeout4
由EXP2,我们可以看出,在微任务队列执行时创建的微任务,还是会排在主线程上创建出的宏任务之前执行(因为微任务只有一条,自增链不断的话 会一直往下执行微任务,不会被中断)
主线程=> 主线程上创建的微任务1=> 微任务1上创建的微任务2=> 主线程上创建的宏任务
script里的代码被列为一个task,放入task队列。
循环1:
循环2:
// 宏任务队列 1
setTimeout(()=> {
// 宏任务队列 1.1
console.log('timer_1');
setTimeout(()=> {
// 宏任务队列 3
console.log('timer_3')
}, 0)
new Promise(resolve=> {
resolve()
console.log('new promise')
}).then(()=> {
// 微任务队列 1
console.log('promise then')
})
}, 0)
setTimeout(()=> {
// 宏任务队列 2.2
console.log('timer_2')
}, 0)
console.log('==========Sync queue==========')
==========Sync queue==========timer_1
new promise
promise then
timer_2
timer_3
主线程(宏任务队列 1)=> 宏任务队列 1.1=> 微任务队列 1=> 宏任务队列 3=>宏任务队列2.2
循环1:
循环2
循环3
循环4
// 宏任务1
new Promise((resolve)=> {
console.log('new Promise(macro task 1)');
resolve();
}).then(()=> {
// 微任务1
console.log('micro task 1');
setTimeout(()=> {
// 宏任务3
console.log('macro task 3');
}, 0)
})
setTimeout(()=> {
// 宏任务2
console.log('macro task 2');
}, 0)
console.log('==========Sync queue(macro task 1)==========');
==========Sync queue(macro task 1)==========new Promise(macro task 1)
micro task 1
macro task 2
task 3
复制代码
异步宏任务队列只有一个,当在微任务中创建一个宏任务之后,他会被追加到异步宏任务队列上(跟主线程创建的异步宏任务队列是同一个队列)
主线程=> 主线程上创建的微任务=> 主线程上创建的宏任务=> 微任务中创建的宏任务
循环1:
循环2
循环2
<div class="outer">
<div class="inner"></div>
</div>
var outer=document.querySelector('.outer');
var inner=document.querySelector('.inner');
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
点击 inner,最终打印结果为:
click
promise
click
promise
timeout
timeout
为什么打印结果是这样的呢?我们来分析一下: (0)将 script 标签内的代码(宏任务)放入执行栈执行,执行完后,宏任务微任务队列皆空。
(1)点击 inner,onClick 函数入执行栈执行,打印 "click"。执行完后执行栈为空,因为事件冒泡的缘故,事件触发线程会将向上派发事件的任务放入宏任务队列。
(2)遇到 setTimeout,在最小延迟时间后,将回调放入宏任务队列。遇到 promise,将 then 的任务放进微任务队列
(3)此时,执行栈再次为空。开始清空微任务,打印 "promise"
(4)此时,执行栈再次为空。从宏任务队列拿出一个任务执行,即前面提到的派发事件的任务,也就是冒泡。
(5)事件冒泡到 outer,执行回调,重复上述 "click"、"promise" 的打印过程。
(6)从宏任务队列取任务执行,这时我们的宏任务队列已经累计了两个 setTimeout 的回调了,所以他们会在两个 Event Loop 周期里先后得到执行。
可以看成是:
function onClick() {
//模拟outer click事件
setTimeout(function(){onClick1()},0)
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
function onClick1() {
console.log('click1');
setTimeout(function() {
console.log('timeout1');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
});
}
//模拟inner click事件
onClick()
inner.click()
打印结果为:
click
click
promise
promise
timeout
timeout
依旧分析一下:
(0)将 script(宏任务)放入执行栈执行,执行到 inner.click() 的时候,执行 onClick 函数,打印 "click"
(1)当执行完 onClick 后,此时的 script(宏任务)还没返回,执行栈不为空,不会去清空微任务,而是会将事件往上冒泡派发
...(关键步骤分析完后,续步骤就不分析了)
可以看成是:
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
onClick();
onClick();
在一般情况下,微任务的优先级是更高的,是会优先于事件冒泡的,但如果手动 .click() 会使得在 script代码块 还没弹出执行栈的时候,触发事件派发。
【执行进入microtask检查点时,浏览器会执行以下步骤:】
总结以上规则为一条通俗好理解的:
[总结]所有的异步都是为了按照一定的规则转换为同步方式执行。
备的一篇干货文章,对你有用的话就收藏或者转发吧!
HTML5 DOM 选择器
javascript 代码
阻止默认行为
javascript 代码
阻止冒泡
javascript 代码
鼠标滚轮事件
javascript 代码
检测浏览器是否支持svg
javascript 代码
检测浏览器是否支持canvas
javascript 代码
检测是否是微信浏览器
javascript 代码
常用的一些正则表达式
javascript 代码
js时间戳、毫秒格式化
javascript 代码
getBoundingClientRect() 获取元素位置
javascript 代码
HTML5全屏
javascript 代码
关注我
定期分享前端干货、新技术解读
如果你越学越迷茫,没思路
加我微信 webwula
备注“想提升自己”
我教你学前端的方法
*请认真填写需求信息,我们会在24小时内与您取得联系。