javascript 高级笔记
一:基础部分
1: var let const
1,使用 var 会出现变量提升,也就是说变量的声明会在代码预解析阶段提到最前面
console.log(sad) //undefind
var sad = 'jjaaj'
2,暂时性死区,使用 let 和 const 就不会出现变量提升,必须先声明后再来使用。let 和 const 有块级作用域,不允许重复声明,var 没有,允许重复声明,使用 var 可能还会影响到 window 里的属性。
var i = 99
for (var i = 0; i < 5; i++) {
console.log(i)
}
console.log(i) //5 var没块级作用域,用let,const就不会出现这些问题
3,全局污染,当一个变量 a 没有声明就直接赋值,那么可能在任何地方都能使用它,污染其他变量 a。
4,关于 const 定义常量,值在同一个作用域下不可以更改,定义一个变量,其实存储的是内存地址的引用,也就是说用常量就不可以修改内存地址的引用,但是如果是一个引用类型,比如对象,你就可以修改里面的值,可以用 object.freeze(xx)冻结。
const obj = {}
obj.name = 'coderz'
2:运算符
1,传值与传址
let a = 1
let b = a //对于基本数据类型,相当于重新开辟了一块内存空间
let obj = {
name: 'coderz'
}
let bio = obj //对于引用数据类型,只是复制了内存地址,没有重新开辟内存空间
2,严格模式,可以避免很多错误 'use strict'。
3,关于一元运算符,++n 与 n++,在表达式中,++n 是先自加在参与运算,n++相反。
4,关于比较运算符,类型不同不可以比较,他会转成 number 类型
let a = 1
let b = '2'
console.log(a > b) //b会转化为number
5,短路运算符
let a = 1
let b = 0
if (a) {
console.log('sss')
} //sss
if (b) {
console.log('sss')
} //不会输出 也就是说,1为真,0为假,undefind,null,空字符串也为假,空对象,数组为真,因为里面有原型
6,三元表达式
//少用if,多用三元表达式
let a = 1 > 2 ? 15 : 44
console.log(a) //44
3:循环
1,关于 switch,没有 break 会发生 case 穿透,遇到 break 才停止。与任何一个 case 不匹配会进入 defalt。
2,while 循环与 do while 循环,后者无论条件怎样都会执行一次
while (a < 5) {
//注意如果a=true会一直循环下去
console.log(a)
}
3,for 循环打印三角
for (let i = 0; i < 10; i++) {
for (let j = 0; j < i; j++) {
document.write('*')
}
document.write('<br>')
} // 输出到控制台的话用'\n'换行
4,for 循环中的 continue(跳过这次循环)与 break(退出循环)。
5,对引用数据类型的遍历,
for…in最好用来遍历对象,但也可以遍历数组(有缺陷)
for…in遍历的是key-value中的key值
let obj = {
name: 'lisi',
age: 45,
sex: '男'
}
for (let item in obj) {
console.log(obj[item])
} //item 得到的是key,而for of得到的是value,刚好相反
需要注意的是,使用for…in遍历对象时,不仅会把对象上的属性遍历出来,还会把对象原型链上的可枚举的属性遍历出来。
为什么说最好不要用for…in遍历数组呢?
(1)for…in遍历出的数组索引为字符串型数字,不能直接进行几何运算
(2)遍历的顺序可能不是按照实际数组的内部顺序
(3)使用for…in会遍历所有的可枚举属性,包括原型
for…of可以用来遍历数/数组/字符串/map/set等拥有迭代器对象的集合\for…of遍历出来的是key-value中的value值
补充:
(1)for…of不能用来遍历对象,因为普通对象中没有迭代器对象
(2)与forEach()不同的是,for…of可以使用break,continue,return等语句
4: 常见字符串方法
let str = 'wosjinsihfd'
//大小写转化
console.log(str.toUpperCase())
console.log(str.toLowerCase())
//去除空格 在js中空格也算字符
str.trim()
//取其中某个字符
str.charAt(索引) //或者用取数组元素的方式
//字符串截取 三种方法
str.slice(开始位置,结束位置) //负数则是向后开始截取 返回一个新字符串
str.substring(开始位置,结束位置) //不支持负数
str.substr(开始位置,结束位置) //这个会截取到结束位置的数,她两是结束位置之前
//字符串检索
srt.indexOf('0',开始查找位置) //注意,包括当前位置,找到了返回索引,没找到返回-1
str.includes() //用法同上,不过返回值是布尔类型
//字符串替换 它会返回新字符串
replace('x','sss')
//字符串重复
str.repeat(次数)
//类型转换,数值转字符串
number+'' //隐式
String(str)
//字符串转数值
Number(str)
parseInt(str2)
parseFloat(str2)
//如果字符串里由非数字呢 str2 = '1212fshfs'
conosle.log(parseint(str2)) //1212 没有数字,转化不了,NAN
//字符串转数组
str.split('') //['s', 'j', 'l', 's', 'g', 'h', 's']
// str.split(separator,howmany) howmany参数:可选。该参数可指定返回的数组的最大长度。如果设置了该参数,返回的子串不会多于这个参数指定的数组。如果没有设置该参数,整个字符串都会被分割,不考虑它的长度.separator参数:必需填。字符串或正则表达式,以什么标准来划分这个字符串(何处开始划分) ''每个字符都划分 'abcdef' 'c' 划分为 ['ab', 'def']
//数组转字符串
str.join('分隔符')
5:数值
数值与布尔类型进行比较
let a = 12
if (a == true) {
console.log('aaa')
} //不会输出,布尔类型会转化为数值类型,true为1,false为0
常见一种情况
let num = 12
if (num) {
console.log('222')
} //222 当num不为空,且不等于0 成立
转布尔类型
//字符串,数字都一样
let a = 12
console.log(!!a) //true
console.log(Bo0lean(a)) //true
数值方法
//判断是否整数
let a = 1
isInteger(a)
// 保留小数点后多少位
a.toFixed(2) //保留小数点后两位,四舍五入
NAN 类型
// 比如对字符串转化,字符串里有字母,就返回NAN
6:Math 对象
//向上,向下取整
Math.ceil(5.01)
Math.floor(5.9)
//取最大最小值
Math.max(1, 2, 3)
Math.min(1, 2, 3)
//四舍五入
Math.round()
随机数
conosle.log(Math.random()) //[0,1)
//两个简单的算法,1,取[0,5] 不用ceil的原因是取到0的几率很小 为啥?
console.log(Math.floor(Math.random() * 6))
//取[2,5] [a,b] a+Math.floor(Math.random()*(b-a+1))
console.log(2 + Math.floor(Math.random() * 4))
7:Date 对象
const date = new Date() //返回对象,括号可以传入日期
const date2 = Date() //返回字符串
//时间戳
const stamp1 = Date.now()
const stamp2 = date * 1
详细
var myDate = new Date()
myDate.getYear() //获取当前年份(2位)
myDate.getFullYear() //获取完整的年份(4位,1970-????)
myDate.getMonth() //获取当前月份(0-11,0代表1月)
myDate.getDate() //获取当前日(1-31)
myDate.getDay() //获取当前星期X(0-6,0代表星期天)
myDate.getTime() //获取当前时间(从1970.1.1开始的毫秒数)
myDate.getHours() //获取当前小时数(0-23)
myDate.getMinutes() //获取当前分钟数(0-59)
myDate.getSeconds() //获取当前秒数(0-59)
myDate.getMilliseconds() //获取当前毫秒数(0-999)
myDate.toLocaleDateString() //获取当前日期
var mytime = myDate.toLocaleTimeString() //获取当前时间
myDate.toLocaleString() //获取日期与时间
8:展开运算符与解构赋值
let arr = ['zs', 545, 65, 454]
let [name, ...arg] = arr
//name=zs arg = [545,65,454]
//再给函数传递参数时,也可以用展开运算符 相当于arguments
const foo = (...args) => {
console.log(args)
console.log(...args)
}
foo(1, 2, 3, 4, 5, 6)
// [1, 2, 3, 4, 5, 6] 将各个参数加到一个数组中(聚拢)
// 1 2 3 4 5 6 怎么输入怎么返回
foo([1, 2, 3, 4, 5])
// [Array(5)] 将数组加到一个数组中
// [1, 2, 3, 4, 5] 怎么输入怎么返回
9:数组元素的操作
//往后添加元素
let a = ['sd', 'ds']
let b = ['s', 'id']
a.push('ha')
a.push(...b) //返回的是改变后数组长度
//往前添加元素,都可以传入多个值,添加删除元素
a.unshift('ss')
//后面开始删除元素 返回值是被删除的元素
a.pop()
//前面开始删除元素
a.shift()
数组截取
let a = [12, 45, 36, 78]
console.log(a.slice(0, 2)) //12,45 slice(start,end) 是截取到结束位置前一位,他是返回了一个新的数组,没有改变原数组
//splice() 改变原数组 可以截取,替换,追加 ,它改变了原来的数组,截取出来扔掉,并且会返回新数组
conosle.log(splice(0, 2)) //12,45,36 splice(start,截取多少位,替换元素)
console.log(a.splice(0, 3, 'hahah')) //[12, 45, 36]
console.log(a) // ['hahah', 78]
清空数组
let arr = [12, 54, 96]
let b = arr //arr地址给b,b清空了,对arr无影响
b = [] // b赋值一个空对象,它重新开辟了一块内存空间
arr.length = 0 //最好用这个
合并
arr.concat(arr2)
翻转
arr.reverce()
元素查找
indexOf()//找到返回索引,没找到返回-1
lastIndexOf()
includes()//用法和字符串那个一样
//好用的find方法
let arr = [12,54,36,85,34]
let asd = arr.find((item)=>{
retun item ===54
})
console.log(asd)//54,返回的是值
//还有个findindex 他返回的就是索引位置
排序
//sort() a-b升序 b-a降序
let a = [12, 55, 36, 110]
a = a.sort((a, b) => {
return b - a
})
console.log(a)
循环
for of
for in
//forEach((item,index,arr)=>{}) arr得到数组
查找数组中是否有满足条件的元素
//some((item,index,arr)) 找到返回true,没有false。找到一个就不在查找了
//还有个every方法,大体上一样,不过必须每一项满足才返回true
let a = [12, 55, 36, 110]
let h = a.some(item => {
return item === 110
})
console.log(h) //true
过滤元素
//filter((item,index,arr)=>{}) 返回过滤后的新数组
let a = [12, 55, 36, 110]
let h = a.filter(item => {
return item === 110
})
console.log(h) //[110]
映射
//map((item,index,arr)=>{}) map() 方法返回一个新数组,新数组中的元素为原始数组中的每个元素调用函数处理后得到的值
const arr=[1,2,3,4,5,61];
const newArr=arr.map(function a (item,i,arr){
item*=2;
return item
});
console.log( newArr)/ / newArr=[2,4,6,8,10,12] //值类型,不会影响到原来的数组
//----------注意,如果是一个引用数据类型,这样做就会影响到原来的值,他们指向同一个内存地址 比如
let course = [
{name:'shuxue',price:45},
{name:'yuwen',price:75},
{name:'dili',price:46},
{name:'yinyu',price:35}
]
course.map((item)=>{
return item.time = 10
})
console.log(course) //{name:'shuxue',price:45,time:10},后面也一样
reduce 函数 是数组原型上的方法每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。
//total没有初始值(初始值可以是空数组,空对象等)的话第一次循环等于12,第二次等于返回值。val第一次等于55,然后依次向后推,没有元素就不循环了
//可以统计某个元素出现的次数 数组求和
let a = [12, 55, 36, 12, 12, 110]
let i = 12
let aaa = a.reduce((total, val, index, arr) => {
total += val == i ? 1 : 0
return total
}, 0)
console.log(aaa)
reduce使用例子
let sum = [0, 1, 2, 3].reduce(function (previousValue, currentValue) {
return previousValue + currentValue
}, 0)
// sum is 6
要累加对象数组中包含的值,必须提供 initialValue,以便各个 item 正确通过你的函数。
let initialValue = 0
let sum = [{x: 1}, {x: 2}, {x: 3}].reduce(function (previousValue, currentValue) {
return previousValue + currentValue.x
}, initialValue)
console.log(sum) // logs 6
let flattened = [[0, 1], [2, 3], [4, 5]].reduce(
( previousValue, currentValue ) => previousValue.concat(currentValue),
[]
)
let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice']
let countedNames = names.reduce(function (allNames, name) {
if (name in allNames) {
allNames[name]++
}
else {
allNames[name] = 1
}
return allNames
}, {})
// countedNames is:
// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }
let people = [ { name: 'Alice', age: 21 }, { name: 'Max', age: 20 }, { name: 'Jane', age: 20 }];
function groupBy(objectArray, property) {
return objectArray.reduce(function (acc, obj) {
let key = obj[property]
if (!acc[key]) {
acc[key] = []
}
acc[key].push(obj)
return acc
}, {})
}
let groupedPeople = groupBy(people, 'age')
// groupedPeople is:
// {
// 20: [
// { name: 'Max', age: 20 },
// { name: 'Jane', age: 20 }
// ],
// 21: [{ name: 'Alice', age: 21 }]
// }
使用 .reduce() 替换 .filter().map()
使用 Array.filter() 和 Array.map() 会遍历数组两次,而使用具有相同效果的 Array.reduce() 只需要遍历一次,这样做更加高效。(如果你喜欢 for 循环,你可用使用 Array.forEach() 以在一次遍历中实现过滤和映射数组)
const numbers = [-5, 6, 2, 0];
const doubledPositiveNumbers = numbers.reduce((previousValue, currentValue) => {
if (currentValue > 0) {
const doubled = currentValue * 2;
previousValue.push(doubled);
}
return previousValue;
}, []);
console.log(doubledPositiveNumbers); // [12, 4]
10:symbol
// symbol是一种基本数据类型。 sybol('描述'),生成永远不会重复的数字或字符串
let a = symbol('啊哈哈哈')
console.log(a.description) //啊哈哈哈
symbol.for('') //开辟新内存空间
symbol.keyFor
11:set
Set对象是值的集合,你可以按照插入的顺序迭代它的元素。Set 中的元素只会出现一次,即 Set 中的元素是唯一的。
//,没有重复的key(同类型) 1和‘1’可以
//set函数可以接受数组,类似数组的对象(字符串)作为参数,用来进行数据初始化,注意初始化时,会自动去重
var s = new Set([1,2,3,3,'3',4])
s.add()//添加
s.delete()//删除
s.clear()//清空
s.size()//长度
s.has()//检查是否有某元素
set 转数组,数组转set
var s = new Set([1,2,3,3,'3',4])
let b = [...s]
可以数组去重,处理并集,交集,差集 用 forEach 遍历
let a = new Set([12, 56, 34, 76])
let b = new Set([35, 12, 76, 95])
// 并集
// let c = new Set([...a,...b])
// 交集
// let c = [...a].filter((item)=>{
// return b.has(item)
// })
// 差集
// let c = [...a].filter((item)=>{
// return !b.has(item)
// })
// console.log(c)
12:垃圾回收
比如一个对象的值,没有人引用了,就会在内存中删除
weakeSet(和 set 类型差不多,不过只能存引用数据类型 )弱引用特性
13:map
对象的键只能是字符串,map 为任意
一些方法
new Map() 创建新的 Map 对象。
set(key,value) 为 Map 对象中的键设置值。
get(key) 获取 Map 对象中键的值。
entries() 返回 Map 对象中键/值对的数组。
keys() 返回 Map 对象中键的数组。
values() 返回 Map 对象中值的数组。
14:类型判断
判断 JS 类型,有以下几种方法:1、typeof 2、object.property.toString.call 3、instance of。
方法一、typeof 方法 基本数据类型除了 null 外都返回对应类型的字符串。
typeof 1 // "number"
typeof 'a' // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof 42n // "bigint"
123456
注:判断一个变量是否被声明可以用(typeof 变量 === “undefined”)来判断 null 返回“object”
typeof null // "object"
1
因为历史遗留的原因。typeof null 尝试返回为 null 失败了,所以要记住,typeof null 返回的是 object。
特殊值 NaN 返回的是 “number”
typeof NaN // "number"
1
而复杂数据类型里,除了函数返回了"function"其他均返回“object”
typeof({a:1}) // "object" 普通对象直接返回“object”
typeof [1,3] // 数组返回"object"
typeof(new Date) // 内置对象 "object"
123
函数返回"function"
typeof function(){} // "function"
1
所以我们可以发现,typeof 可以判断基本数据类型,但是难以判断除了函数以外的复杂数据类型。于是我们可以使用第二种方法,通常用来判断复杂数据类型,也可以用来判断基本数据类型。
方法二、object.property.toString.call 方法 ,他返回"[object, 类型]",注意返回的格式及大小写,前面是小写,后面是首字母大写。
基本数据类型都能返回相应的类型。
Object.prototype.toString.call(999) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(Symbol()) // "[object Symbol]"
Object.prototype.toString.call(42n) // "[object BigInt]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(true) // "[object Boolean]
1234567
复杂数据类型也能返回相应的类型 Object.prototype.toString.call({a:1}) // “[object Object]” Object.prototype.toString.call([1,2]) // “[object Array]” Object.prototype.toString.call(new Date) // “[object Date]” Object.prototype.toString.call(function(){}) // “[object Function]” 这个方法可以返回内置类型
方法三、obj instanceof Object ,可以左边放你要判断的内容,右边放类型来进行 JS 类型判断,只能用来判断复杂数据类型,因为 instanceof 是用于检测构造函数(右边)的 prototype 属性是否出现在某个实例对象(左边)的原型链上。
[1,2] instanceof Array // true
(function(){}) instanceof Function // true
({a:1}) instanceof Object // true
(new Date) instanceof Date // true
1234
obj instanceof Object 方法也可以判断内置对象。
缺点:在不同 window 或者 iframe 间,不能使用 instanceof。
15:js类型
基本
-
String
-
Number
-
Boolean
-
Null
-
Undefined
-
Symbol
基本数据类型存在栈内存
引用
Array,Function,Object. 除了基本数据类型,都是引用数据类型
引用数据类型存储在堆内存
根据 JavaScript 对语言类型的分类,很容易知道,并不是 JavaScript 万物皆对象,或者说任何非基本类型的都是对象类型。
既然基本类型并非对象,也就不具备属性和方法,那为什么能使用例如length, charAt的方法的了?这其中起作用的就是基本包装函数了。
每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据( null, undefined 没有对应的构造函数形式)。
一个简单例子:
let str = 'Jack'
let oStr = str.substring(2)复制代码
第二行代码,访问 str 时,访问过程处于读取模式,也就是会从内存中读取这个字符串的值,在读取过程中,会进行以下几步:
- 创建一个 String 类型的一个实例;
- 在实例上调用相应的方法。
- 销毁这个实例。
另一种形式表示:
let str = new String('Jack')
let oStr = str.substring(2)
str = null复制代码
基本包装函数,与引用类型主要区别就是对象的生存期,使用 new 操作符创建的引用类型的实例,在执行流离开当前作用域之前一直都保存在内存中,而自动创建的基本包装类型的对象,则只存在与一行代码的执行瞬间,然后被立即销毁。这也就是不能给基本类型添加属性和方法的原因了。
二:函数
//可以对函数进行编组
let obj = {
asd: function () {},
djfhd: function () {}
}
声明一个全局函数,其实会压入 window 对象里,但是用匿名函数赋值给变量不会(不能用 var)。
let asd = function () {}
具名函数会函数提升,用匿名函数不会。
函数其实也是引用类型
1:立即执行函数
顾名思义,声明一个函数并马上调用这个函数就叫做立即执行函数;也可以说立即执行函数是一种语法,让你的函数在定义以后立即执行;并且该函数只会执行一次,执行后自动被垃圾回收。
立即执行函数又叫做自执行函数
立即执行函数前的一个函数(不只是函数)必须要有分号,保险起见,可以直接在立即执行函数的前面添加“;”
;(function (参数) {})(给参数传的值)
立即执行函数好处
//通过定义一个匿名函数,创建了一个新的函数作用域,相当于创建了一个“私有”的命名空间,该命名空间的变量和方法,不会破坏污染全局的命名空间。此时若是想访问全局对象,将全局对象以参数形式传进去即可
2:默认参数
let a = function (s = 10, b = 12) {}
a(11, 11) //传入了参数会覆盖,不传用默认
函数的参数可以为任意类型
4:不确定参数
有时候传入的参数个数是不确定的
//arguments 是一个对象,不是一个 Array 。它类似于Array ,但除了length属性和索引元素之外没有任何Array 属性。例如,它没有 pop 方法。但是它可以被转换为一个真正的Array
let a = function () {
console.log(arguments)
console.log([...arguments]) //转数组
}
a(12, 54, 63, 93)
//也可以用展开运算符
let a = function (...arg) {
console.log(arg)
}
a(12, 54, 63, 93)
5:递归
//只用考虑两层
function sum(...args) {
if (args.length === 0) {
return 0
}
return args.pop() + sum(...args)
}
console.log(sum(1, 3, 7, 9, 1))
三:this
1:Js 中 this 的理解
this 的指向不是在函数定义时决定的,而是函数执行时决定的 this 指向调用时所在的对象
//this绑定的四种方式
//1. this的默认绑定window
//凡是函数作为独立函数调用,没有指定调用对象, 无论位置在哪里,this默认绑定window对象(非严格模式)
//示例如下:
function test(){
console.log(this == window);
}
test(); // 打印true
function outFun(){
function inFun(){
console.log(this == window);
}
inFun();
}
outFun(); // 打印true
//2. this的隐式绑定 方法
//示例1:
var person = {
age: 12,
onTest: function(){
console.log(this.age);
}
}
person.onTest(); // 输出12
//当函数被一个对象包含的时候,函数的this被隐式的绑定到这个对象, 这时可以通过this访问绑定对象的其他属性
//示例2:
function onTest(){
console.log(this.age);
}
var person1 = {
age: 12,
onTest: onTest
}
person1.onTest(); // 输出12
onTest(); // 输出 undefined
person1.onTest() 也可以取到age, 可以看出函数this的绑定并不会因为它被定义在j对象的内部和外部而有任何区别
//示例3:
var person2 = {
age: 12,
onTest: function(){
console.log(this.age);
}
}
function callTest(fn){
fn()
}
callTest(person2.onTest); // 输出undefined
//函数this的隐式绑定是动态的
//由于函数的this与对象是动态绑定的, 在对象调佣函数之前函数的this和对象没有强制绑定,函数传递存在this绑定丢失问题
//3.this的显示绑定(使用call 和 bind)
var person3 = {
age: 12,
onTest: function(){
console.log(this.age);
}
}
function callTest(fn){
fn.call(person3); // 硬绑定
}
callTest(person3.onTest); // 输出12
//使用call() 方法,传递丢失的this绑定成功绑定到person3
var person3 = {
age: 12,
onTest: function(){
console.log(this.age);
}
}
var fn = person3.onTest;
fn(); // 输出undefined
var onTest2 = fn.bind(person3);
onTest2(); // 输出12
//使用bind() 方法,得到一个this和对象绑定的方法
//call和bind的区别是:在绑定this到对象参数的同时
//1.call将立即执行该函数
//2.bind不执行函数,只返回一个可供执行的函数
//4.new绑定
function fun(a){
this.name = a;
}
var obj1 = new fun('aa');
var obj2 = new fun('bb');
console.log(obj1.name);
console.log(obj2.name);
//执行new操作的时候,将创建一个新的对象,并且将构造函数的this指向所创建的新对象
//5.注意
//箭头函数:不使用这四个this规则,根据词法作用域来决定this。
隐式绑定的一些场景:
- 全局上下文默认 this 指向 window,严格模式下指向 undefined;
- 直接调用函数,this 相当于全局上下文的情况;普通函数里指向 window。定时器也是 window
- 对象.方法名 的情况引用,this 指向这个对象;obj.eat()
- 构造函数中的 this 指向实例对象;
- DOM 事件绑定中的 this 指向绑定事件的元素;IE 浏览器中比较特殊指向 window;
- 箭头函数的 this 指向当前最近的非箭头函数的 this,找不到的话,指向 window;
引深:怎么改变 this 的指向?有哪些方法?它们有什么区别?有没有写过 bind 函数的实现?
显示修改 this 的三种方法:(函数的方法) 不需要改变 this 就传 Null
call(某对象,arg1,arg2,...)
apply(某对象,[arg1,arg2,...])
bind(某对象,arg1,arg2) bind 会返回一个新函数
有时候我们使用 call 或者 apply 的目的不在于指定 this 指向,而是另有用途,比如借用其他对象的方法。那么我们可以传入 null 来代替某个具体的对象
区别:1. 传参方式;2. bind 只绑定 this,不调用函数,call 和 apply 即绑定 this,又调用函数(立刻执行)
2:箭头函数 this
箭头函数本身没有 this,它继承父级作用域的 this
四:环境与作用域
1:全局环境不会被回收,
全局会渗透到在它之下定义的函数,对象。。。里面可以用外面的,反之不行。
//一个函数
function a() {
let age = 18
}
a()
a()
a() //函数每调用一次会开辟一块新的内存空间(里面的数据也是全新的),调用完毕销毁,所以age并不是共享的
//如果函数在使用,那么同作用域下的变量就会被保留
//javascript中,事件处理函数创造的环境不会被销毁,因为用户随时可能会调用。
2:闭包
一以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
前面我们知道了子作用域可以访问到父作用域的数据
//利用闭包特性取数组某一区间
let arr = [12, 56, 34, 92, 1, 34, 6, 78]
function between(a, b) {
return function (item) {
//可以访问到父级作用域的数据
return item > a && item < b
}
}
console.log(arr.filter(between(12, 40)))
一个案例,按钮的移动效果(注意,动画要加定位才能移动)
let btn = document.querySelector('button')
btn.addEventListener('click', function () {
let lefts = 1
setInterval(() => {
btn.style.left = lefts++ + 'px'
}, 100)
})
//点击按钮,按钮往右边移动,但是再次点击,按钮出现抖动,原因是再次点击,开辟了新环境,lefts又变成1了
//解决,将lefts提到上一级作用域下
//但是出现了新问题,按钮移动越来越快,因为创建了多个定时器,解决
let btn = document.querySelector('button')
let lefts = 1
let bind = false
btn.addEventListener('click', function () {
if (!bind) {
bind = true
setInterval(() => {
btn.style.left = lefts++ + 'px'
}, 100)
}
})
防抖节流
闭包的内存泄漏问题,由于依赖其他作用域下的数据,
let btn = document.querySelector('button')
btn.addEventListener('click', function () {
console.log(desc.getAttribute('desc'))
console.log(btn)
})
//事件处理函数依赖btn,所以btn不会被销毁,占用内存
//改造后
let btn = document.querySelector('button')
desc = btn.getAttribute('desc')
btn.addEventListener('click', function () {
console.log(desc)
console.log(btn)
})
btn = null
console.log(btn)
五:对象
1:基 础
获取对象属性
user.xxx
user['xxx']
//第二种应用场景
for (let key in user) {
console.log(user[key])
}
删除属性
delete user.xxx
对象也可以用展开运算符,解构赋值
解构赋值默认值
let arr = [12, 65, 34]
let [a, b, c = 77] = arr //没有c的话默认为77
对象属性检测
user.hasOwnPropperty('xxx') //只检测自身,不会检测原型
console.log('xxx' in user) //都检测
对象合并
Object.assign(object1, object2)
获取对象所有属性。值
object.key(user)
object.values(use)
//都获取
object.entries()
循环
for in
//注意,for of不行,他是操作迭代对象的。数组具有迭代特性
2:对象浅拷贝与深拷贝(重点)
浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
//一些方法
let obj = {
name: 'zs',
age: 56,
sex: '男'
}
//拓展运算符
let clone = { ...obj }
//循环
for (let key in obj) {
clone[key] = obj[key]
}
//Object.assign,ES6中拷贝对象的方法,接受的第一个参数是拷贝的目标,剩下的参数是拷贝的源对象(可以是多个)
//Object.assign还有一些注意的点是:
//不会拷贝对象继承的属性
/*不可枚举的属性
属性的数据属性/访问器属性
可以拷贝Symbol类型*/
Object.assign(target, ...sources)
深拷贝
浅拷贝只在根属性上在堆内存中创建了一个新的的对象,复制了基本类型的值,但是复杂数据类型也就是对象则是拷贝相同的地址,而深拷贝则是对于复杂数据类型在堆内存中开辟了一块内存地址用于存放复制的对象并且把原有的对象复制过来,这 2 个对象是相互独立的,也就是 2 个不同的地址.
/*JSON.stringify()是目前前端开发过程中最常用的深拷贝方式,原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新的对象*/
/*通过JSON.stringify实现深拷贝有几点要注意
拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
无法拷贝不可枚举的属性,无法拷贝对象的原型链
拷贝Date引用类型会变成字符串
拷贝RegExp引用类型会变成空对象
对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null
无法拷贝对象的循环应用(即obj[key] = obj)*/
//递归
let obj = {
friends: {
name: 'zs'
}
}
function deepClone(obj) {
// 创建空对象这步很重要,相当于开辟一块新的内存空间
let res = {}
for (let key in obj) {
if (typeof obj[key] === 'object') {
res[key] = deepClone(obj[key])
} else {
res[key] = obj[key]
}
}
return res
}
let obj2 = deepClone(obj)
obj2.friends['name'] = 'lisi'
console.log(obj)
console.log(obj2)
3:构造函数和普通函数区别
创建方式
字面量创建,构造函数创建(new) ,字符串,数值,等都可以通过构造函数创建,所以才能直接使用构造函数里的方法。
1、调用方式不一样
//构造函数也是一个普通函数,创建方式和普通函数一样。
function Foo(){}
Foo();//普通函数调用方式
var f = new Foo();//构造函数调用方式
普通函数调用方式:直接调用person();
构造函数调用方式:需要使用new关键字来调用 new person();
//2、作用也不一样(构造函数用来新建实例对象)
3、首字母大小写习惯
一般构造函数的函数名称会用大写
普通函数用小写
4、函数中this的指向不同
普通函数中的this,在严格模式下指向undefined,非严格模式下指向window对象。
function foo(){
console.log(this===window);
}
foo();
//代码运行结果:true
构造函数的this则是指向它创建的对象实例。
function Foo(){
this.name = "令狐冲";
}
var f = new Foo();
console.log(f.name);
//代码运行结果:令狐冲
//补充:构造函数的函数名和类名相同:Foo()这个构造函数,Foo是函数名,也是这个对象的类名。
5、写法的不同
构造函数:
function Person(name){
this.name = name;
}
var p = new Person('John');//使用new关键字,不使用return
普通函数:
function person(name){
var obj = new Object();
obj.name = name;
return obj;//使用return
}
var p = person('john');
function person(name){
this.name = name;
return this;//使用return
}
var p = person('john'),
构造函数作用:构造函数主要作用是用于实例化类
4:new 的时候发生了什么
- 自动创建一个空对象
- 为新创建的对象添加proto,将该属性链接至构造函数的原型对象
- 把空对象和构造函数里的 this 衔接起来(this 指向实例化对象)
- 隐式返还
this(即该函数没有返回对象,则返回this);返回这个对象。
5:object 里的 API
object.isExtensible(obj) 禁止向对象添加属性
Object.seal(obj) 封闭对象(不可增删改 isseal()判断是否封闭
Object.freeze(obj)冻结对象
6:对象访问器
让对象里的数据更可控
const user = {
data: { name: 'zs', age: '12' },
//注意,访问器会覆盖你直接设置的属性
set age(val) {
if (typeof val !== number) {
throw new Error('年龄格式错误')
}
this.data.age = val
},
get age() {
return this.data.age
}
}
//这样在外部就不能随意修改数据
user.age = '88' //出错
7:proxy 代理
vue2采用Object.defineProperty劫持数据
vue3采用proxy
为什么取代:Object.defineProperty监听不到数组的变化 解决:Vue.Set this.$set()
proxy:ie9以下不兼容
vue3:proxy接触数据几个步骤
1,数据劫持2,观察者3,数据解析{{}},指令转化为js对象,然后通过diff算法转化为dom4,
8:JSON
一种通用的格式,适合多语【之间传递数据 json.stringfy(xxx,要保留的属性,缩进) json.parse(xxx,(key,value)=>{ //对键值处理 })
六:原型,构造函数
概念:
prototype:原型
_ proto _:原型链(连接点,类似于指针)
从属关系:1.prototype是函数的一个属性,是一个对象 作用:共享方法,一些公共的方法就可以放到prototype上 Star.prototype.eat
=function(){}
2._ proto _是对象Object的一个属性,也是对象 不能直接使用这个属性
3.对象的 _ proto _保存着该对象构造函数的prototype
4.最顶层是Object.protoytpe
constructor构造函数
prototype和_ proto _里面都有一个constructor属性,他称为构造函数,因为他指回构造函数本身,主要用于记录该对象引用于那个构造函数,他可以让prototype重新指向原来的构造函数。
构造函数类似于一张设计图
如果修改了原来的prototype,给他赋值的是一个对象,则必须手动的用constructor指回原来的构造函数。
function Test(){
this.name = 'zs'
}
//1
consoloe.log(Test.protoype)
//2
const test = new Test()
console.log(test._prototype_)
//3
cosnole.log(test._prototype_ === Test.protoype)//true
console.log(Test.protoype._prototype_ === Object.prototype)//true
console.log(Object.prototype._prototype_)//NULL
//通过—proto—一层层向上查找
Test.prototype.age = 18
Test.prototype._proto_.sex = '男'
console.log(test.name)//zs
console.log(test.age)//18
console.log(test.sex)//男
//FUnction与Object
//他两既是函数也是对象
//Test 是由FUnction构造出来的
console.log(Test._proto_ === Function.prototype)//true
console.log(Function._proto_ === Function.prototype)//true
cosnole.log(Object._proto_ === Function.prototype)//true
console.log(Object._proto_ === FUnction._proto_)//true
//判断属性是否存在的方法
test.hasOwnProperty('xxx')
//判断属性是否存在于原型链上
console.log(xxx in test)
//constructor
consoe.log(test.constructor === Test)//true
//test.constructor是可以改变的
七:函数防抖与节流
防抖:触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
见应用场景:用户注册时的手机号码验证和邮箱验证
节流:高频事件触发,但在n秒内只会执行一次,节流会稀释函数的执行频率
常用场景:监听页面元素滚动事件,因为页面元素滚动是个高频触发事件
1.防抖
在事件触发n秒后才开始执行的回调-》延迟执行。
若果在n秒内再次触发了事件,重新开始计时。
分为两种情况,1第一次点击即开始延时,2第一次不延时。
//注意两个问题,this指向问题,事件对象的获取问题
//首先,页面一刷新就会执行debounce函数,点击才会执行返回的函数
<button>点击</button>
<script>
document.querySelector('button').addEventListener('click',debounce(submit,2000,true),false)
function submit(e){
console.log(e)
}
function debounce(fn,timer,triggleNow){
let t = null
return function(){
// 这个其实就是事件的回调函数
// 这个function传入的参数个数不确定,所以把arguments给fn
// this指向的是事件触发对象(button),也传给fn
let arg = arguments
let _this = this
// 清除定时器
if(t){
clearTimeout(t)
}
if(triggleNow){
// 第一次不延时
let isfirstClick = !t
if(isfirstClick){
// 第一次点击
fn.apply(_this,arg)
}
t = setTimeout(function(){
t = null
},timer)
}else {
t = setTimeout(function(){
fn.apply(_this,arg)
// 用apply(_this,arg)而不用fn(arg)是因为前者,其中的数组元素将作为单独的参数传给 fn 函数,在submit里就能更直观方便的使用
},timer)
}
}
}
</script>
2.节流
function throttle(fn,delay){
let begin = 0
return function (){
let cur = new Date().getTime()
if(cur - begin > delay){
fn.apply(this,arguments)
begin = cur
}
}
}
八:类
使用class创建类是es6新出的语法糖
class Start{
//属性
constructor(uname){
this.uname = uname
}
//方法
sing(){
console.log("hahaha")
}
}
let a = new Start('薛之谦')
//constructor,只要new就会调用这个函数,不写也会自动生成
继承 extends
可以使用super关键字调用父类的构造函数或者普通函数,注意,必须先调用父类的构造方法再调用子类的
1.es6之前的继承
使用构造函数+protype实现 称为组合继承
function Father(uname){
this.uname = uname
}
Father.prototype.getmoney = function(){
console.log(10000)
}
function Son(uname,uage){
//调用父构造函数,改变this指向 实现了属性的继承
Father.call(this,uname)
this.uage = age
}
//那么方法的继承呢
Son.prototype = Father.prototype
//这样是不行的,因为子prototype指向了父prototype,prototype是一个对象,如果在子prototype里修改属性,会影响父prototype
//解决
Son.prototype = new Father()
//但是要注意,用对象的形式修改了prototyoe,原来的prototyoe被覆盖,constructor指向会出问题,要修改constructor的指向,指回原来的构造函数
Son.prototype.constructor = Son
九:promise
js是单线程的(防止dom冲突)
1.同步与异步
异步运行机制
1。所有同步任务都在主线程上执行,形成一个执行栈(execution context stack) 2。主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件 3。一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行 4。主线程不断重复上面的第三步。这个过程叫事件循环。
宏任务与微任务
宏:计时器,ajax,读取文件
微:promise.then()
执行顺序
同步任务 process.nextTick 微任务 宏任务 setlmmediate
异步使用场景
1)定时任务:setTimeout,setInverval 2)网络请求:ajax请求,img图片的动态加载 3)事件绑定或者叫DOM事件,比如一个点击事件,我不知道它什么时候点,但是在它点击之前,我该干什么还是干什么。用addEventListener注册一个类型的事件的时候,浏览器会有一个单独的模块去接收这个东西,当事件被触发的时候,浏览器的某个模块,会把相应的函数扔到异步队列中,如果现在执行栈中是空的,就会直接执行这个函数。 4)ES6中的Promise 异步与回调
回调函数不一定属于异步,一般同步会阻塞后面的代码,通过输出结果也就得出了这个结论。 回调函数,一般在同步情境下是最后执行的,而在异步情境下有可能不执行,因为事件没有被触发或者条件不满足。
回调地狱
function getTea(fn){
setTimeout(()=>{
fn()
},500)
}
function getHotpot(fn2){
setTimeout(()=>{
fn2()
},2000)
}
getTea(function(){
console.log('喝奶茶')
})
getHotpot(function(){
console.log('吃火锅')
})
喝奶茶执行时间比吃火锅早,如果想先吃火锅呢(也就是改变代码执行循序,或者一个回调依赖另一个回调的结果)
function getTea(fn){
setTimeout(()=>{
fn()
},500)
}
function getHotpot(fn2){
setTimeout(()=>{
fn2()
},2000)
}
//层层嵌套,形成回调地狱
getHotpot(function(){
console.log('吃火锅')
getTea(function(){
console.log('喝奶茶')
})
})
使用promise改造 (把异步操作放进promise对象里)
function getTea(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('喝奶茶')
},500)
})
}
function getHotpot(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('吃火锅')
},1500)
})
}
getHotpot().then((res)=>{
console.log(res)
return getTea()
}).then((res)=>{
console.log(res)
})
2.promise定义
promise对象用来封装一个异步操作并可以获取其成功/失败的结果值。
- 阮一峰的解释: 所谓
Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果 从语法上说,Promise是一个对象,从它可以获取异步操作的消息Promise提供统一的 API,各种异步操作都可以用同样的方法进行处理
3.Promise 的状态
实例对象promise中的一个属性 PromiseState
pending 变为 resolved/fullfilled pending 变为 rejected 注意
对象的状态不受外界影响 只有这两种,且一个 promise 对象只能改变一次 一旦状态改变,就不会再变,任何时候都可以得到这个结果 无论成功还是失败,都会有一个结果数据。成功的结果数据一般称为 value,而失败的一般称为 reason。
4.promise基本流程
5.基本使用
Promise构造函数接受一个函数(执行器函数)作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数value传递出去; reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数error/reason传递出去。
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
then方法可以接受两个回调函数作为参数。 第一个回调函数onResolved()是Promise对象的状态变为resolved时调用 第二个回调函数onRejected()是Promise对象的状态变为rejected时调用 这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数
6.promise.all()
Promise.all()创建的Promise会在这一组Promise全部解决后在解决。也就是说会等待所有的promise程序都返回结果之后执行后续的程序。返回一个新的Promise。
- 栗子
let p1 = new Promise((resolve, reject) => {
resolve('success1')
})
let p2 = new Promise((resolve, reject) => {
resolve('success1')
})
// let p3 = Promise.reject('failed3')
Promise.all([p1, p2]).then((result) => {
console.log(result) // ['success1', 'success2']
}).catch((error) => {
console.log(error)
})
// Promise.all([p1,p3,p2]).then((result) => {
// console.log(result)
// }).catch((error) => {
// console.log(error) // 'failed3'
//
// })
复制代码
有上述栗子得到,all的性质:
- 如果所有都成功,则合成Promise的返回值就是所有子Promise的返回值数组。
- 如果有一个失败,那么第一个失败的会把自己的理由作为合成Promise的失败理由。
7.Promise.race()
Promise.race()是一组集合中最先解决或最先拒绝的Promise,返回一个新的Promise。
- 栗子
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success1')
},1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed2')
}, 1500)
})
Promise.race([p1, p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 'success1'
})
复制代码
有上述栗子得到,race的性质:
- 无论如何,最先执行完成的,就执行相应后面的.then或者.catch。谁先以谁作为回调
8.异步终极解决方案
async 是“异步”的简写,而 await 可以认为是 async wait 的简写。await 只能出现在 async 函数中。async 用于申明一个 function 是异步的,而 await 操作符用于等待异步执行完毕。
async与await分别都有语法,两种语法结合就可以使异步代码像同步代码一样
async起什么作用
用于声明函数是一个异步函数。 声明的方式
//异步函数的声明方式
async function f1(){}
let fn2=async function(){}
let fn3=async()=>{}
async用于返回/输出的是一个 Promise 对象。 如果没有return 则默认返回 Promise.resolve() Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
返回非promise对象,肯定是成功的状态
如果是promise对象,就有里面的结果决定,返回的值,也是里面promise对象的值
默认return Promise.resolve()
return 常量 等于 return Promise.resolve('常量')
await起什么作用
await 是在等待一个 async函数(异步函数)执行完成之后的一个返回值,后面跟直接量或者普通函数都是可以的(不过这时候的await就是个摆设 没什么用的),如果等到的返回值是Promise.resolve(变量),await还会直接 resolve函数传出来的值。await等待的时候就不会执行后面的语句。
return 常量 等于 Promise.resolve('常量')
await语句后的Promise对象变成reject状态时,那么整个async函数会中断,后面的程序不会继续执行
//await 只能等待成功的状态 失败需要抛出异常
async function f3() {
try {
var z = await Promise.reject(30);
} catch (e) {
console.log(e); // 30
}
}
结合 asiox 使用
asiox 返回的就是一个promise实例,可以直接使用 await
axios 使用 await
结合 Promise.all方法,Promise.race方法
Promise.all全部resolve才算resolve,Promise.race 谁的状态先改变,先返回谁 无论是resolve还是reject(如果时间一样,数组中排前面的先触发);all和race的参数都是一个数组,两个方法返回的都是promise对象,是个异步;
Promise.all 使用 await
async、await与setTimeout结合使用 sleep
await并不能等到setTimeout,如果没有return 那么默认返回 undefined
return 常量 等于 Promise.resolve('常量')
休眠n秒以后执行其后面的语句
//sleep
function sleep(s){
return new Promise(function(resolve,reject){
setTimeout(()=>{
resolve();
},s*1000)
})
}
async function doit(){
await sleep(2);
// 等两秒后执行别的
}
doit();
十:进程与线程
进程是程序的一次执行过程,是一个动态概念,是操作系统资源分配的基本单位 线程是任务调度和执行的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源
- 一个程序至少有一个进程,一个进程至少有一个线程
- 线程的划分尺度小于进程,使得多线程程序的并发性高
- 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
- 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
十一:数组相关题
1.数组去重
方法 1 扩展运算符和 Set 结构相结合,就可以去除数组的重复成员
// 去除数组的重复成员
[...new Set([1, 2, 2, 3, 4, 5, 5])];
// [1, 2, 3, 4, 5]
复制代码
方法 2 Array.from()能把set结构转换为数组
Array.from(new Set([1, 2, 2, 3, 4, 5, 5]));
// [1, 2, 3, 4, 5]
复制代码
方法 3(ES5)
function unique(arr) {
let temp = [];
arr.forEach(e => {
if (temp.indexOf(e) == -1) {
temp.push(e);
}
});
return temp;
}
5.字符串翻转
方法1
let str = 'i am a pig'
//转数组
let str2 = str.split(' ')
//数组翻转
let str3 = str2.reverse()
//转回字符串
console.log(str3.join(' '))
6.数组翻转
1.使用reverce()
let arr = [1,3,5,7,9]
function revorces(arr){
let arrLenth = arr.length
let index = arrLenth-1
let newArr = []
while(index>=0){
newArr.push(arr[index])
index--
}
return newArr
}
console.log(revorces(arr))
十二:栈与堆
栈数据结构
栈是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端称为栈顶。 栈被称为是一种后入先出(LIFO,last-in-first-out)的数据结构。 由于栈具有后入先出的特点,所以任何不在栈顶的元素都无法访问。 为了得到栈底的元素,必须先拿掉上面的元素。
在这里,为方便理解,通过类比乒乓球盒子来分析栈的存取方式。
这种乒乓球的存放方式与栈中存取数据的方式如出一辙。 处于盒子中最顶层的乒乓球 5,它一定是最后被放进去,但可以最先被使用。 而我们想要使用底层的乒乓球 1,就必须将上面的 4 个乒乓球取出来,让乒乓球1处于盒子顶层。 这就是栈空间先进后出,后进先出的特点。
堆数据结构
堆是一种经过排序的树形数据结构,每个结点都有一个值。 通常我们所说的堆的数据结构,是指二叉堆。 堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。 由于堆的这个特性,常用来实现优先队列,堆的存取是随意,这就如同我们在图书馆的书架上取书, 虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书, 我们只需要关心书的名字。
变量类型与内存的关系
基本数据类型
基本数据类型共有6种:
- Sting
- Number
- Boolean
- null
- undefined
- Symbol
基本数据类型保存在栈内存中,因为基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。
为了更好的搞懂基本数据类型变量与栈内存,我们结合以下例子与图解进行理解:
let num1 = 1;
let num2 = 1;
复制代码
PS: 需要注意的是闭包中的基本数据类型变量不保存在栈内存中,而是保存在堆内存中。这个问题,我们后文再说。
引用数据类型
Array,Function,Object...可以认为除了上文提到的基本数据类型以外,所有类型都是引用数据类型。
引用数据类型存储在堆内存中,因为引用数据类型占据空间大、大小不固定。 如果存储在栈中,将会影响程序运行的性能; 引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。 当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体
为了更好的搞懂变量对象与堆内存,我们结合以下例子与图解进行理解。
// 基本数据类型-栈内存
let a1 = 0;
// 基本数据类型-栈内存
let a2 = 'this is string';
// 基本数据类型-栈内存
let a3 = null;
// 对象的指针存放在栈内存中,指针指向的对象存放在堆内存中
let b = { m: 20 };
// 数组的指针存放在栈内存中,指针指向的数组存放在堆内存中
let c = [1, 2, 3];
复制代码
因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量中获取了该对象的地址指针, 然后再从堆内存中取得我们需要的数据。
\