一、事件循环
浏览器有哪些线程
- GUI渲染线程---负责渲染页面,例如解析html,css,布局并渲染页面
- js引擎线程---最出名的就是我们熟知的V8引擎,解析并执行js
- 事件触发线程
- 定时器触发线程
- 异步请求线程---浏览器会新开一个线程进行请求,当检测到请求状态发生变化时,如果设置了回调函数,会将此函数放入任务队列中。
JavaScript并不是一行一行的分析并执行代码的,而是分段一段一段的进行分析并执行。这里要说明一下,怎么才算一段。这里的段指的是JavaScript中的执行上下文。
执行上下文分为三种
- 全局执行上下文----这里指的就是全局代码
- 函数执行上下文----这里指的是一个一个函数,一个函数执行前会先创建一个执行上下文
- eval执行上下文----eval不推荐使用,这里不做过多说明
JS内存机制
在JS中,每一个数据都需要一个内存空间。内存空间又被分为两种,栈内存(stack)*与*堆内存(heap)。
对象放在heap(堆)里,常见的基础类型和函数放在stack(栈)里,函数执行的时候在栈里执行。栈里函数执行的时候可能会调一些Dom操作,ajax操作和setTimeout定时器,这时候要等stack(栈)里面的所有程序先走**(注意:栈里的代码是先进后出)**,走完后再走WebAPIs,WebAPIs执行后的结果放在callback queue(回调的队列里,注意:队列里的代码先放进去的先执行),也就是当栈里面的程序走完之后,再从任务队列中读取事件,将队列中的事件放到执行栈中依次执行,这个过程是循环不断的。
var a = "aa";
function one(){
let a = 1;
two();
function two(){
let b = 2;
three();
function three(){
console.log(b)
}
}
}
console.log(a);
one();
最先走的肯定是three,因为two要是先销毁了,那three的代码b就拿不到了,所以是先进后出(先进的后出),所以,three最先出,然后是two出,再是one出。
任务队列的分类
js主线程它是有一个执行栈的,所有的js代码都会在执行栈里运行。在执行代码过程中,如果遇到一些异步代码(比如setTimeout,ajax,promise.then以及用户点击等操作),那么浏览器就会将这些代码放到一个线程(在这里我们叫做幕后线程)中去等待,不阻塞主线程的执行,主线程继续执行栈中剩余的代码,当幕后线程(background thread)里的代码准备好了(比如setTimeout时间到了,ajax请求得到响应),该线程就会将它的回调函数放到任务队列中等待执行。而当主线程执行完栈中的所有代码后,它就会检查任务队列是否有任务要执行,如果有任务要执行的话,那么就将该任务放到执行栈中执行。如果当前任务队列为空的话,它就会一直循环等待任务到来。因此,这叫做事件循环。
js是有两个任务队列的,一个叫做Macrotask Queue(Task Queue),一个叫做Microtask Queue.
接下来看看都有哪些属于宏任务,哪些属于微任务。
宏任务(macrotask):script( 整体代码)、setTimeout、setInterval、I/O(http请求)、UI 渲染、DOM 事件
微任务(microtask ):Promise.then()、MutationObserver(监听dom的更改)、
console.log(1)
setTimeout(function () {
console.log(2)
}, 0);
new Promise(function (resolve) {
console.log(3)
resolve()
})
.then(function () {
console.log(4)
})
.then(function () {
console.log(5)
})
console.log(6)
注意:new Promise中的是同步代码,Promise.then()是微任务
二、array
concat
concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2);
console.log(array3);
// expected output: Array ["a", "b", "c", "d", "e", "f"]
copyWithin()
copyWithin() 方法浅复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组的长度。
copyWithin(target, start, end)
target:复制序列到该位置。如果是负数,target 将从末尾开始计算。如果 target 大于等于 arr.length,将不会发生拷贝。
start:开始复制元素的起始位置。如果是负数,start 将从末尾开始计算。如果 start 被忽略,copyWithin 将会从 0 开始复制。
end:开始复制元素的结束位置。copyWithin 将会拷贝到该位置,但不包括 end 这个位置的元素。如果是负数, 将从末尾开始计算。如果 end 被忽略,copyWithin 方法将会一直复制至数组结尾(默认为 arr.length)。
[1, 2, 3, 4, 5].copyWithin(-2)
[1, 2, 3, 4, 5].copyWithin(0, 3)
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
at()
at()* 方法接收一个整数值并返回该索引对应的元素,允许正数和负数。负整数从数组中的最后一个元素开始倒数。
在传递非负数时,at() 方法等价于括号表示法。例如,array[0] 和 array.at(0) 均返回第一个元素。但是,当你需要从数组的末端开始倒数时,则不能使用 array[-1],因为方括号内的所有值都会被视为字符串属性,因此你最终读取的是 array["-1"],这只是一个普通的字符串属性而不是数组索引。
const cart = ['apple', 'banana', 'pear'];
console.log(cart[-1]);
console.log(cart.at(-1));
every()
every() 测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。
备注: 若收到一个空数组,此方法在任何情况下都会返回 true。
[12, 5, 8, 130, 44].every(x => x >= 10);
filter()
filter() 方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。
filter(element,index,array)
element:数组中当前正在处理的元素。
index:正在处理的元素在数组中的索引。
array:调用了 filter() 的数组本身。
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
array.filter(item=>item>10)
find()
find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
find(element,index,array)
const inventory = [
{name: 'apples', quantity: 2},
{name: 'bananas', quantity: 0},
{name: 'cherries', quantity: 5}
];
inventory.find(item=>item.name==='apples')
findIndex
findIndex()*方法返回数组中满足提供的测试函数的第一个元素的*索引。若没有找到对应元素则返回 -1。
findLast()
findIndex()*方法返回数组中满足提供的测试函数的第一个元素的*索引。若没有找到对应元素则返回 -1。
findLastIndex()
findLastIndex() 方法返回数组中满足提供的测试函数条件的最后一个元素的索引。若没有找到对应元素,则返回 -1。
flat()
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。(扁平化数组)
flat(depth) depth:指定要提取嵌套数组的结构深度,默认值为 1。
var arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
flatMap()
flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它与 map 连着深度值为 1 的 flat 几乎相同,但 flatMap 通常在合并成一种方法的效率稍微高一些。
var arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flatMap(item => item);
forEach
Array.from()
Array.from() 方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
console.log(Array.from('foo'));
console.log(Array.from([1, 2, 3], x => x + x));
includes()
includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
[1, 2, NaN].includes(NaN); // true
indexOf
Array.isArray()
Array.isArray() 用于确定传递的值是否是一个 Array。
const arr = [1,2]
Array.isArray(arr)
join()
join() 方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串,用逗号或指定的分隔符字符串分隔。如果数组只有一个元素,那么将返回该元素而不使用分隔符。
keys()
keys() 方法返回一个包含数组中每个索引键的 Array Iterator 对象。
var arr = ["a", , "c"];
var denseKeys = [...arr.keys()];
console.log(denseKeys);
lastIndexOf()
方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。
lastIndexOf(searchElement, fromIndex)
searchElement:被查找的元素。
fromIndex:从此位置开始逆向查找。默认为数组的长度减 1(arr.length - 1),即整个数组都被查找。如果该值大于或等于数组的长度,则整个数组会被查找。如果为负值,将其视为从数组末尾向前的偏移。即使该值为负,数组仍然会被从后向前查找。如果该值为负时,其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。
var array = [2, 5, 9, 2];
var index = array.lastIndexOf(2);
var index = array.lastIndexOf(2, 2);
console.log(index)
pop() 方法从数组中删除最后一个元素,并返回该元素的值。此方法会更改数组的长度。
push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。
slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。
unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度。
splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的
some()
some() 方法测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值。
如果用一个空数组进行测试,在任何情况下它返回的都是false。
some() 方法会循环数组中的数据,当某个数据符合条件时,将不再循环数组,返回一个true 。如果当前数组中数据都不复合条件,则返回false;回调函数中不写return 则返回false;
[2, 5, 8, 1, 4].some(item=>item>7)
reduce()
reduce() 方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。
reduce((previousValue, currentValue, currentIndex, array) => { /* … */ }, initialValue)
一个“reducer”函数,包含四个参数:
-
previousValue:上一次调用callbackFn时的返回值。在第一次调用时,若指定了初始值initialValue,其值则为initialValue,否则为数组索引为 0 的元素array[0]。 -
currentValue:数组中正在处理的元素。在第一次调用时,若指定了初始值initialValue,其值则为数组索引为 0 的元素array[0],否则为array[1]。 -
currentIndex:数组中正在处理的元素的索引。若指定了初始值initialValue,则起始索引号为 0,否则从索引 1 起始。 -
array:用于遍历的数组。initialValue可选作为第一次调用
callback函数时参数 previousValue 的值。若指定了初始值initialValue,则currentValue则将使用数组第一个元素;否则previousValue将使用数组第一个元素,而currentValue将使用数组第二个元素。const array1 = [1, 2, 3, 4]; let initialValue = 0 array1.reduce((previousValue, currentValue) => { initialValue += currentValue } , initialValue) console.log(initialValue)
reverse() 方法将数组中元素的位置颠倒,并返回该数组。数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个。该方法会改变原数组。
三、Reflect
ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
var myObject = {
foo: 1,
};
// 旧写法
'foo' in myObject // true
// 新写法
Reflect.has(myObject, 'foo') // true
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。有了Reflect对象以后,很多操作会更易读。
Reflect对象一共有 13 个静态方法。
-
Reflect.apply(target, thisArg, args):等同于
Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数。一般来说,如果要绑定一个函数的
this对象,可以这样写fn.apply(obj, args),但是如果函数定义了自己的apply方法,就只能写成Function.prototype.apply.call(fn, obj, args),采用Reflect对象可以简化这种操作。// 老写法 Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1 // 新写法 Reflect.apply(Math.floor, undefined, [1.75]) // 1 -
Reflect.construct(target, args):等同于
new target(...args),这提供了一种不使用new,来调用构造函数的方法。function Greeting(name) { this.name = name; } // new 的写法 const instance = new Greeting('张三'); // Reflect.construct 的写法 const instance = Reflect.construct(Greeting, ['张三']); -
Reflect.get(target, name, receiver):查找并返回
target对象的name属性,如果没有该属性,则返回undefined。 -
Reflect.set(target, name, value, receiver):设置
target对象的name属性等于value。 -
Reflect.defineProperty(target, name, desc):基本等同于
Object.defineProperty,用来为对象定义属性。 -
Reflect.deleteProperty(target, name):等同于
delete obj[name],用于删除对象的属性。 -
Reflect.has(target, name):对应
name in obj里面的in运算符。 -
Reflect.ownKeys(target):返回对象的所有属性,基本等同于
Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。 -
Reflect.isExtensible(target):对应
Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展。 -
Reflect.preventExtensions(target):
Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。 -
Reflect.getOwnPropertyDescriptor(target, name):用于得到指定属性的描述对象,基本等同于
Object.getOwnPropertyDescriptor -
Reflect.getPrototypeOf(target):读取对象的
__proto__属性,对应Object.getPrototypeOf(obj)。Reflect.getPrototypeOf和Object.getPrototypeOf的一个区别是,如果参数不是对象,Object.getPrototypeOf会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf会报错。Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0} Reflect.getPrototypeOf(1) // 报错 -
Reflect.setPrototypeOf(target, prototype):设置目标对象的原型(prototype),对应
Object.setPrototypeOf(obj, newProto)方法。它返回一个布尔值,表示是否设置成功。
四、Proxy
Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
下面是 Proxy 支持的拦截操作一览,一共 13 种。
- get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo和proxy['foo']。 - set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v或proxy['foo'] = v,返回一个布尔值。 - has(target, propKey):拦截
propKey in proxy的操作,返回一个布尔值。 - deleteProperty(target, propKey):拦截
delete proxy[propKey]的操作,返回一个布尔值。 - ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 - defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 - preventExtensions(target):拦截
Object.preventExtensions(proxy),返回一个布尔值。 - getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy),返回一个对象。 - isExtensible(target):拦截
Object.isExtensible(proxy),返回一个布尔值。 - setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 - construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)。
vue3数据响应式原理的实现:
-
通过Proxy代理实现
-
通过Reflect(反射)被代理的对象,属性进行操作
const person = { name: '小浪', age: 22, } const p = new Proxy(person, { // 有人读取p 身上的某个属性 get(target, propName) { console.log(`有人读取了p身上的${propName}属性`); return Reflect.get(target, propName) }, // 给p对象身上增加/ 修改某个属性时调用 set(target, propName, value) { console.log(`有人修改/增加了p身上的${propName}属性`); Reflect.set(target, propName, value) }, deleteproperty(target, propName) { console.log(`有人删除了p身上的${propName}属性`); return Reflect.deleteProperty(target, value) // 删除一个属性 } })