文章目录
堆和栈
内存: 是计算机的运行空间
内存颗粒: 内存中分为独立的空间,空间中有独立的区域,其中最小的叫内存颗粒
内存颗粒中分为两个区域 栈 堆,
栈和堆默认关系: 一对一,多对一(两个栈中的地址对应一个堆的空间)
栈:
1. 保存的是 对象的(地址),函数调用时传递的是变量的(值)
2 . 空间小,稳定,里面都是地址 所以不可被修改
3. 先进后出
js主线程的函数执行都压在这里
堆:
1. 保存的是值,数据
2. 空间大,里面的是值 所以可被修改。
3. 先进先出
垃圾回收就是检查这里
栈
var a = 10
var b = a
b = 20
console.log(a) //10
console.log(b) //20
// 基本类型 (值传递的数据)
// 复制的就是值
// 除了对象和函数
堆
var o ={name:'admin'}
var o2 = o
o2.name = 'root'
console.log(o) //root
console.log(o2) //root
//复杂类型 (引用类型,引用传递的数据)
//复制的是地址,同一个地址,指向同一个值
//对象和函数
数据类型分类:
值传递:(基本数据)(栈)
内存中,值就是地址,地址就是值
引用传递:(复杂数据)(堆)
内存中,地址就是地址,值就是值
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。
当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体
深浅拷贝
1.深浅拷贝的区别
浅拷贝
只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
修改新对象会改到原对象。
深拷贝
会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
2.实现深拷贝的方法
(json)实现深拷贝
let obj={
name:'zs',
age:18
}
obj2 = JSON.parse( JSON.stringify(obj) )
obj.age = 12
console.log(obj) //name:'zs',age:18
console.log(obj2) //name:'zs',age:12
// JSON不支持NaN,Infinity,甚至精确的浮点数,更别说循环引用和function了。
// 如果要深拷贝的对象属性值为undefined或者是function的时候,会被过滤掉!
(for-in遍历)实现深拷贝
let obj={
name:'zs',
age:18
}
let obj3 = {}
for(let attr in obj){
obj3[sttr] = obj[attr]
}
obj.age = 12
console.log(obj) //name:'zs',age:18
console.log(obj3) //name:'zs',age:12
函数库lodash的_.cloneDeep方法
var _ = require('lodash')
var obj = {
a: {
c: 2,
d: [9, 8, 7]
},
b: 4
}
var obj1 = _.cloneDeep(obj)
console.log(obj === obj1);//false
递归实现
function deepClone(obj){
let objClone = Array.isArray(obj)?[]:{};
if(obj && typeof obj==="object"){
for(key in obj){
if(obj.hasOwnProperty(key)){
//判断obj子元素是否为对象,如果是,递归复制
if(obj[key]&&typeof obj[key] ==="object"){
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
let a=[1,2,3,4],
b=deepClone(a);
a[0]=2;
console.log(a,b);
(Object.assign)实现深拷贝
//对象的合并
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
如果对象的属性值为简单类型(string,number),得到的新对象为深拷贝;
如果属性值为对象或其他引用类型,那对于这个对象而言其实是浅拷贝的,
(…)实现深拷贝
let obj={
name:'zs',
age:18
}
let obj1 = {...obj}
obj.age = 12
console.log(obj) //name:'zs',age:18
console.log(obj1) //name:'zs',age:12
如果只是一层数组或是对象,其元素只是简单类型的元素,那么属于深拷贝
如果数组或对象中的元素是引用类型的元素,那么就是浅拷贝
作用域
对象 | 类型 |
---|---|
global/window | 全局作用域 |
{ } | 块级作用域 es6 |
this | 动态作用域 |
举例:
全局: {}内未定义的,当我们打开网页,自动生成全局作用域
局部: 函数 或{}内部
从外层的作用域无法直接访问函数内部的作用域!
如果想读取函数内的变量,必须借助 return 或者闭包。
作用域对变量的限制:(作用域链)
1.变量只能在自己的作用域使用。
2.作用域的变量,可以往上查找,使用上级作用域的变量,
不能往下查找,不能使用下级作用域的变量。
(局部有上下级之分,只能从里往上不能往下)
作用域的赋值:
先自己,有-直接赋值,没有-再往上,
最后全局
如果全局没有,则给他定义为全局变量,再给他赋值
变量提升
就是把所有的变量声明提升到当前的作用域最前面,
(把var 提升到最前面(不赋值),剩下的依次罗列)
var sco ="aaa";
function fn(){
console.log(sco);
var sco ="bbb"
}
fn(); //undefined
上面的代码输出是undefined,这是因为局部变量sco 变量提升了,等效于下面
var sco ="aaa";
function fn(){
var sco;
console.log(sco);
sco ="bbb"
}
fn(); //undefined
for循环中let和var的区别
for中var声明的是全局变量,由于变量提升的机制,var命令只在最初执行一次,后面的都是覆盖执行,所以最后显示的是循环完了的值
let不存在变量提升,所以每循环一次就会声明一个新变量
var li =document.querySelectorAll('li')
// 使用var 无论使用哪一个输出都是长度
for(var i=0; i<li.length;i++){
li[i].onclick=function(){
console.log(i);
}
}
// 使用let 点击哪一个则对应哪一个的下标
for(let i=0; i<li.length;i++){
li[i].onclick=function(){
console.log(i);
}
}
闭包:
闭包是指有权访问另一个函数作用域中变量的函数,简单理解就是 一个作用域可以访问另一个函数内部的局部变量
function b () {
var a = 1
return function c () {
console.log(a)
}
}
var e = b()
e()//输出1
闭包的用途:
延长变量作用域。在函数的外部可以访问函数内部的局部变量,容易造成内存泄漏,
因为闭包中的局部变量影响不会被回收
递归
递归是自己调用自己,深拷贝是通过深度递归实现的,
同步异步
同步: 一个一个执行
异步: 一起执行
例
计时器,循环中的事件,事件中获取循环的变量,
数据请求
进程:
一个功能的开始执行到执行结束的过程
线程:
一个功能在执行过程中的每一个分支。多线程异步执行,线程越多,执行速度越快,但需要消耗大量性能
解决异步的四种方法:
1.回调函数(容易造成回调地狱)
2.promise
打印:
3.generator(属于微任务)
4. async+await(可与promise混用)
宏任务和微任务
js是一门单线程语言,所以它本身是不可能异步的,但是js的宿主环境(比如浏览器、node)是多线程,宿主环境通过某种方式(事件驱动)使得js具备了异步的属性。而在js中,我们一般将所有的任务都分成两类,一种是同步任务,另外一种是异步任务。而在异步任务中,又有着更加细致的分类,那就是微任务和宏任务。
宏任务:
<script>整体代码
setInterval() setTimeout 定时器 延时器
Ajax、
DOM事件
异步队列
微任务:
async/await
(Promise .then 放在异步队列-微任务)
nextTick
Promise (立即执行,同步任务)
执行顺序
例1
主 console.log('1')
宏 setTimeout(function(){
console.log('2')
},0)
同步 new Promise(function(resolve){
console.log('3')
resolve()
异队 }).then(function(){
console.log('4')
})
输出: 1 3 4 2
例2
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')
})
})
输出:1、7 、6、8、2、4、3、5、9、11、10、12
答案 :
第一轮 执行外面同步代码 : 1 7
第二轮 执行 微任务 : 6 8
第三轮 宏任务 第一个setTimeout : 同步 2 4 微任务 3 5 第二个setTimeout:同步 9 11 微任务 10 12
注意:
1.一个宏任务里面要是有微任务,等里面代码全部执行完,
再去执行下一个宏任务
2.如果两个JS文件,等第一个JS文件里面宏和微全部执行完再执行下一个JS文件,
对于宏任务和微任务请记住这几点:
微任务比宏任务执行要先输出。
宏任务里如果有宏任务,不会执行里面的那个宏任务,而是被丢进任务队列后面,
所以会最后执行。
promise
Promise 本身是同步的,是异步的一种解决方案,
Promise 有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
一旦执行就无法改变
在promise中接受两个内置参数分别是resolve(成功)和reject(失败),
Promise实例生成以后,可以用then方法分别指定resolved状态(成功)和rejected状态(失败)的回调函数。
then方法可以传递两个回调函数第一个是成功,第二个是失败,失败回调也可以使用promise的catch方法回调,
解决了回调地狱的问题,可以链式调用,
Promise.all() 并发处理多个异步任务,所有任务都执行完成才能得到结果
Promise.race() 并发处理多个异步任务,只要一个任务完成就能得到结果
白话:
解决回调地狱:例:ajax请求成功之后,再次去请求,则会一层一层嵌套,嵌套会形成闭包,闭包会导致变量一直在内存存储,导致内存泄露,
它自己本身是同步的,是异步的一种解决方案, .then方法的回调函数中再返回一个promise对象,这个promise对象 作为then方法的返回结果,然后一层一层调用、结合async await 使用,解决promise里不停.then的问题
async、await
async 放在函数前面,会将其后函数的返回值封装成一个 Promise 对象,
而 await 只能放在async里面,会等待这个 Promise 完成,并返回结果。
可以使异步代码看起来像同步代码一样,解决promise里不停.then的问题
async定义的异步函数与普通promise对象的回调函数(又称执行函数)是一样的,会立即执行,并不会阻塞后面的代码。
async 和 await 相⽐直接使⽤ Promise 来说,优势在于处理 then 的调⽤链,
能够更清晰准确的写出代码。缺点在于滥⽤ await 可能会导致性能问题,因为 await 会阻塞代码
new 在执行时会做四件事情:
在内存中创建一个新的空对象。
让this 指向这个新的对象。
执行构造函数里面的代码,给这个新对象添加属性和方法。
返回这个新对象(所以构造函数里面不需要return)。
原型链
对象就是实例
任何一个函数只要被new使用了,这个函数就是一个构造函数
解释:
什么是原型链?
所有引用类型都有一个__proto__(隐式原型)属性,属性值是一个普通的对象
所有函数都有一个prototype(原型)属性,属性值是一个普通的对象
所有引用类型的__proto__属性指向它构造函数的prototype
当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找
即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,
这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
继承:
让一个不具有某些功能或属性的类或对象,通过某些方式 使用另一个具有这些功能或属性的类或对象的功能或属性
构造函数继承(改变this指向继承)
简单方便 多继承 只能继承构造函数内部的属性和方法,
function Parent(n,s){
this.name = n
this.sex = s
this.show =function(){
console.log(this.name+ '和'+this.sex)
}
}
function Child(n,s){
Parent.call(this,n,s)
Parent.apply(this,[n,s])
}
var p = new Parent('张三','男')
p.show()
var c = new Child('李四','女')
c.show()
原型对象继承
只能继承原型身上的属性和方法,不能继承构造函数身上的属性和方法
function Parent(n,s){
this.name = 'admin'
}
Parent.prototype.show =function(){
console.log(this.name)
}
function Child(){
this.name = 'root'
}
Child.prototype = Parent.prototype
//改写show 注意深浅拷贝
for(let i in Parent.prototype){
Child.prototype[i] = Parent.prototype[i]
}
Child.prototype.show =function(){
console.log('这是改写之后的show')
}
var p = new Parent()
p.show()
var c = new Child()
c.show()
原型链继承
既能继承构造函数,又能继承原型
不方便传参
function Parent(n,s){
this.name = n
}
Parent.prototype.show =function(){
console.log(this.name)
}
function Child(){}
Child.prototype = new Parent('李四')
var p = new Parent('张三')
p.show()
consloe.log(p) // name:张三
var c = new Child()
c.show()
consloe.log(c) // name:李四
**组合继承 **
既可以继承原型,又可以继承构造函数,传参也方便,麻烦
function Parent(n,s){
this.name = n;
this.sex = s;
}
Parent.prototype.show =function(){
console.log(this.name+ '和' +this.sex )
}
function Child(n,s){
Parent.call(this,n,s)
}
for(let i in Parent.prototype){
Child.prototype[i] = Parent.prototype[i]
}
var p = new Parent('张三','男')
p.show()
consloe.log(p)
var c = new Child('李四','男')
c.show()
consloe.log(c)
class继承 – es6
class Parent{
constructor(n){
this.name=n
}
show(){
console.log(this.name)
}
}
class Child extends Parent{
constructor(n){
super(n)
}
}
var p = new Parent('张三')
p.show()
var c =new Child('李四')
c.show()
构造函数继承(改变this指向继承)
简单方便 多继承 只能继承构造函数内部的属性和方法,
原型对象继承
只能继承原型身上的属性和方法,不能继承构造函数身上的属性和方法
原型链继承
既能继承构造函数,又能继承原型,不方便传参
组合继承
既可以继承原型,又可以继承构造函数,传参也方便,麻烦
class继承 – es6
既可以继承原型,又可以继承构造函数,传参也方便,方便
防抖节流
防抖就是将频繁触发的事件函数变为最后一次执行
节流就是将多次变为每隔一段时间执行
防抖 用于场景
在页面滚动的时候做一些逻辑,scroll DOM频繁操作,优化实现,
浏览器自带的防抖节流工具 requestAnimationFrame 以及 延时器
debounce (func, delay) {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
垃圾回收机制
标记清除(mark and sweep)
大部分浏览器以此方式进行垃圾回收,当变量进入执行环境(函数中声明变量)的时候,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”,在离开环境之后还有的变量则是需要被删除的变量。标记方式不定,可以是某个特殊位的反转或维护一个列表等。
垃圾收集器给内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上的标记的变量即为需要回收的变量,因为环境中的变量已经无法访问到这些变量
引用计数(reference counting)
这种方式常常会引起内存泄漏,低版本的IE使用这种方式。机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时就会被回收,该方式会引起内存泄漏的原因是它不能解决循环引用的问题
javaScript 延迟加载方式
-
defer 属性: HTML 4.01 为
回流重绘
回流一定会触发重绘,而重绘不一定会回流
回流,会改变元素的几何尺寸,影响布局
添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变
——边距、填充、边框、宽度和高度,内容改变
重绘,不会改变元素的几何尺寸。不会影响布局的,比如颜色改变
当render tree中的一些元素需要更新属性,
而这些属性只是影响元素的外观,风格,而不会影响布局的,
导致回流发生的一些因素:
页面初始化渲染
调整窗口大小
改变字体
增加或者移除DOM元素
内容变化,比如用户在 input 框中输入文字, CSS3 动画等
内外边距
操作class属性
脚本操作DOM
计算offsetWidth和offsetHeight属性
设置 style 属性的值