JavaScript基础(四)

JavaScript基础(四)

发表于 2024-08-12
更新于 2024-08-24
阅读时长:30分钟
阅读量:17
AI 总结
|

本文内容来源于网络、网课以及作者自己的归纳总结,js系列文章共计四篇,知识点比较多也比较杂,文章仅作简单的总结,不作过多深入分析。系列文章将持续更新。如有不妥之处,请在评论区友好指出~

1、ES6+小语法

1.1 块级作用域

之前学习var声明时,我们知道var一个变量,会在GO中进行保存,值一开始为undefined,但是这是ECMA规范早期版本的说法,最新的说法是保存在VE变量环境中(VariableEnvironment)

每一个执行上下文都会关联到一个变量环境,当我们用let const var声明一个变量或者函数,它们会被作为环境记录添加到环境变量中。对于函数来说,参数也会被作为环境记录添加到变量环境中

在ES5中只有两种作用域:全局作用域、函数作用域

1、全局作用域

var name = 'linming'
//name声明在全局作用域中

2、函数作用域

function foo() {
    //函数作用域
}

在ES6中,存在块级作用域。但是这个块级作用域对let / const / function / class声明的类型是有效的,对var声明的类型无效

{
  var baz = "xiaoming";
  let foo = "ming";
  const bar = "lin";
  function demo() {}
  class People {}
}
console.log(baz); //能访问,说明块级作用域无效
console.log(foo); //访问不了,报错
console.log(bar); //访问不了,报错
console.log(demo); //访问不了,报错
console.log(People); //访问不了,报错

其中,函数是比较特殊的,不同的浏览器有不同的实现,部分浏览器为了兼容以前的代码,默认让function没有块级作用域

另外,var声明的大括号不是块级作用域

var obj = {
    //不存在块级作用域
}

if语句、switch语句、for循环语句也有块级作用域

if(true) {
    let lastName = 'ming'
    var age = 22
    }
 console.log(lastName) //报错
 console.log(age) //22
var color = "red"
switch (color) {
    case "red":
    var foo = "foo"
    let bar = "bar"
}
console.log(foo) //foo
console.log(bar) //报错
<body>
    <button>按钮</button>
    <button>按钮</button>
    <button>按钮</button>
    <button>按钮</button>
</body>
<script>
    let btn = document.getElementsByTagName('button')
    for (var i = 0; i < btn.length; i++) {
        btn[i].onclick = function() {
            console.log(`第${i}个按钮的点击`);
        }
    }
</script>

这种用var的for循环,当点击按钮后,会去访问全局的i,而此时全局的i已固定为4,所以不论点击哪个按钮,都显示第4个按钮的点击,用let可以很好的解决这个问题。

特别注意,在普通的for循环中不能使用const,但在for...of循环中可以

for (const i = 0; i < 10; i++) {...} //报错

for(const i of arr) {...} //b

1.2 标签模板字符串

模板字符串有一种比较少见的用法:标签模板字符串

function foo() {
    console.log("---")
}

foo() // 普通函数的调用

foo`` // 通过模板字符串调用

如果有字符串的参数,第一个参数是模板字符串中的的全部字符串(放在数组中)

function foo(m, n) {
    console.log(m, n, "--");
}

foo`hello world` //[ 'hello world' ] undefined --

如果传入参数时,插入了其他变量

function foo(m, n, x) {
    console.log(m, n, x, '------')
}

const name = "ming"
const age = 18
foo`hello${name}world${18}` // ['hello', 'world', ''] 'ming' 18 '------'

第一个参数依然是模板字符串中整个字符串,只是被切成了多块,放到了一个数组中

第二、三个参数,依次是模板字符串中的变量

1.3 Symbol

1、概念

Symbol(符号)是ES6中新增的一个基本数据类型

2、为什么需要这个类型?

在ES6之前,对象的属性名都是字符串形式,很容易造成属性名冲突

例如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性

// ES6之前,对象的属性名不能重复
const obj = {
    name: 'linming'
}

obj["name"] = "小明"  //原先的会被覆盖

Symbol的作用:Symbol可以解决属性命名冲突的问题,可以生成一个独一无二的值

3、Symbol的基本使用

Symbol值是通过Symbol函数来生成的,生成后可以作为属性名(在ES6中,属性名可以使用字符串,也可以使用Symbol值)

//Symbol函数执行后每次创建出来的值都是独一无二的
const s1 = Symbol()
const s2 = Symbol()

console.log(s1 === s2) //false

在ES2019中,Symbol还有一个描述(description)

const s3 = Symbol("aaa")
console.log(s3.description) //输出:aaa

4、Symbol值作为key

方式一

//在定义对象字面量时使用
const s1 = Symbol()
const s3 = Symbol()
const obj = {
    [s1]: "hello",
    [s2]: "world"
}

方式二

//新增属性
const s3 = Symbol()
obj[s3] = "linming"

方式三

const s4 = Symbol()
Object.defineProperty(obj, s4, {
    enumerable: true,
    configurable: true,
    writable: true,
    value: 'lihua'
})

5、获取值

一般获取方式:

obj[s1]

不能通过 . 语法获取

obj.s1  //undefined

不能通过一般的方式来获取key

Object.keys(obj) //[]
Object.getOwnPropertyNames(obj) //[]

需要通过Object.getOwnPropertySymbols来获取所有Symbol的key

console.log(Object.getOwnPropertySymbols(obj))

//遍历
const sKeys = Object.getOwnPropertySymbols(obj)
for(const sKey of sKeys) {
    console.log(obj[sKey])
}

1.4 includes

在ES7之前,如果我们想要判断一个数组中是否包含某一个元素,需要通过indexOf获取结果,并且判断是否为-1

ES7中新增了includes来判断一个数组中是否包含一个指定的元素,返回布尔值

const arr = [10, 20, "hello", 30]

//旧方式
console.log(arr.indexOf(20)) //返回索引值,找不到返回-1

console.log(arr.includes(10)) //true

注:includes的第二个参数可以指定从什么位置开始查找

1.5 指数运算符

//ES7之前
const result1 = Math.pow(3,3) //计算3的3次方
//输出: 27

ES7新增了 **运算符,适用于指数运算

//ES7
const result2 = 3 ** 3
console.log(result2) //输出 27

1.6 Object的遍历

Object.values

我们可以通过Obejct.keys获取一个对象所有的key,在ES8中提供了Object.values来获取所有的value值

const obj = {
    name: 'xiaoming',
    age: 22
}
Object.keys(obj) //输出:[ 'name', 'age' ]
Object.values(obj) //输出:[ 'xiaoming', 22 ]

一般用来遍历对象,但是也是可以传入数组或者字符串

Object.values(["abc", "cba", "nba"]) //输出:['abc', 'cba', 'nba']
Object.values("abc") //输出: ["a", "b", "c"]

Object.entries

获取键值对(结果放入数组中)

const obj = {
    name: "xiaoming",
    age: 22
}
console.log(Object.entries(obj))
//输出:[ [ 'name', 'xiaoming' ], [ 'age', 22 ] ]

1.7 填充pad

padStart:从开头位置开始填充

padEnd:从末尾位置开始填充

const msg = "hello"
console.log(msg.padStart(5, "*")) //输出;*****hello
console.log(msg.padEnd(5, "-"))  //输出:hello-----

1.8 flat()

flat()方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

const arr = [10, 20, [2, 4], [[30, 40], [20, 12]], 44, [55]]
//上方的数组中存在着二维数组和三维数组

flat方法可以将其转变成一个一维的数组

const newArr = arr.flat()
console.log(newArr)
//输出:[ 10, 20, 2, 4, [ 30, 40 ], [ 20, 12 ], 44, 55 ] 是一个二维数组

//可以再调用一次flat,或者直接在原数组调用flat时,传入一个要遍历的深度
const newArr = arr.flat(2)
console.log(newArr)
//输出:[ 10, 20,  2,  4, 30, 40, 20, 12, 44, 55 ]

手动实现该flat函数的方法:遍历数组中的每一个元素,如果元素还是一个数组,递归调用

Array.prototype.myFlat = function (num = 1) {
  const arr = this;
  let newArr = [];
  for (let val of arr) {
    if (Array.isArray(val) && num > 0) {
      newArr.push(...val.myFlat(num - 1));
    } else {
      newArr.push(val);
    }
  }
  console.log(newArr);
  return newArr;
};

1.9 flatMap

flatMap方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组(该方法十分接近于数组的map方法)

1、flatMap是先进行map操作,再做flat的操作

2、flatMap中的flat相当于深度为1

const message = ["hello world", "hi linming", "good morning"]

//使用flatMap
const newArr = message.flatMap(item => item.split(" "))
console.log(newArr)
//输出:[ 'hello', 'world', 'hi', 'linming', 'good', 'morning' ]


//如果采用map(map不会自动将其进行降维)
const newArr = message.map(item => item.split(" "))
console.log(newArr)
//输出:[ [ 'hello', 'world' ], [ 'hi', 'linming' ], [ 'good', 'morning' ] ]

1.10 trimStart、trimEnd

ES5的时候,我们使用trim方法来去除首尾空格

const str ="   hello wolrd     "
console.log(str.trim())
//输出:hello wolrd

使用trimStart、trimEnd可以更加精准的控制

const str = "    hello"
console.log(str.trimStart()) //输出:hello


const str2 = "    hello     "
console.log(str.trimEnd) //输出:     hello

1.11 bigInt

由于js数字精度问题,在js中所能表示的最大安全整数为:

const maxInt = Number.MAX_SAFE_INTEGER
console.log(maxInt)
//输出:9007199254740991

如果想要获取更大的数,就要使用bigInt了

//最大安全整数的100倍
const bigInt = 900719925474099100n //n是必要的

//增加100
const num = 100
console.log(bigInt + BigInt(num))

1.12 空值合并运算??

在以前我们经常使用 || 来表示或运算

let num = 100
console.log(num || "default value") //输出:100

num = null
console.log(num || "default value") //输出:default value

num = undefined
console.log(num || "default value") //输出:default value

但是,逻辑或是有弊端的

如果,num就是一个空字符串,或者就是一个0呢

num = 0
console.log(num || "default value") //期望输出:0

num = ""
console.log(num || "default value") //期望输出:""

结果都输出:default value

所以ES11增加了空值运算符来解决这个问题

num = 0
console.log(num ?? "default value") //输出:0

num = ""
console.log(num ?? "default value") //输出:""

1.13 可选链

const obj = {
    name: 'lihua',
    friend: {
        name: 'lin',
        girlfriend: {
            name: 'ting'
        }
    }
}

//访问lihua的朋友的女朋友的姓名
console.log(obj.friend?.girlfriend?.name)

这样写会使得代码更加严谨,不会因为其中的某个值为undefined时,而报错,从而造成后续代码不能执行

二、Set和Map

ES6之前,存储数据结构的主要有两种方式:数组、对象

ES6新增了另外的两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap

2.1 Set的基本使用

Set(集合),可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复

1、创建Set

const set = new Set()

2、添加元素

set.add(10)
set.add(20)
set.add(30)
console.log(set) //输出:Set(3) { 10, 20, 30 }

可以添加任意类型的数据,但是不能添加重复数据

set.add(10) //重复
console.log(set) //输出:Set(3) { 10, 20, 30 } 重复的10会被移除

可添加多个空对象,因为空对象的引用指向是不同的

set.add({})
set.add({})
console.log(set) //输出:Set(5) { 10, 20, 30, {}, {} }

3、对数组进行去重

const arr = [10,20,30,10,20]
const newArr = Array.from(new Set(arr)) //方式一
const newArr2 = [...new Set(arr)] //方式二

console.log(newArr2);  //输出:[ 10, 20, 30 ]

4、size属性

//获取长度
const arr = [19, 20, 21]
const set = new Set(arr)
console.log(set.size); //3

5、delete方法

set.delete(21)

6、has方法

set.has(20) //true

7、clear方法

set.clear() //清空

2.2 weakSet

和Set相似的另外一个数据结构称之为WeakSet,也就是内部元素不能重复的数据结构

1、创建weakSet

const weakSet = new WeakSet()

2、只能存放对象类型

weakSet.add(10) //报错

weakSet.add([10]) //ok

3、存放对象是一个弱引用关系

Set中对象的引用都是强引用并不会允许垃圾回收;

WeakSet中的对象的引用是弱引用,也就是说,即使WeakSet引用了某个对象,但垃圾回收不把这种引用计为“引用”,只要其他地方没有强引用引用这个对象,该对象就不可达,任何时候都可能被回收;

强引用与弱引用

强引用(string reference):当垃圾回收(GC)认为该引用有效,不会对该对象进行回收

弱应用(weak reference):垃圾回收器认为该引用无效,将其进行回收(并且该对象没有其他引用时)

上图的情况为weakSet时,当obj为null时,GC不认为0x200对0x100的连接有效,所以GC会将其视为垃圾对象

2.3 Map的基本使用

Map是ES6新增的数据结构,用于存储映射关系

基本作用:事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key),某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象成字符串来作为key

1、允许使用对象类型作为key

const obj = {
    name: 'lin'
}
const map = new Map()
map.set(obj, "ming") //key为obj
map.set(1, "hi") // key为基本数据类型

console.log(map)
//输出:Map(2) { { name: 'lin' } => 'ming', 1 => 'hi' }

或者也可以这样子创建

const map = new Map([[obj, "ming"], [1, "hi"]])
console.log(map)
//输出:Map(2) { { name: 'lin' } => 'ming', 1 => 'hi' }

2、创建的属性和方法

//新增属性
map.set("ming", "aaa")

//获取属性
map.get("ming")

//has(key)
map.has("ming") //true

//delelte(key)
map.delete("ming")  // true

//clear() 清空
map.clear()

3、遍历

map.forEach((item, key) => {
    console.log(item, key)
})

for(const [key, value] of map2) {
    console.log(key, value)
}

2.4 weakMap

和Map类型相似的另外一个数据结构称之为weakMap,也是以键值对的形式存在

1、和Map的区别

区别一:weakMap只能使用对象,不接受其他的类型作为key

const weakmap = new WeakMap()
weakmap.set(obj, "aaa")  //ok
weakmap.set(1, "bbb") //报错

区别二:weakmap的key对象的引用是弱引用,如果没有其他对象引用这个对象,那么GC可以回收该对象

const obj = {
    name: 'lin'
}
const weakmap = new WeakMap()
weakmap.set(obj, "aaa")

当obj被设置为null时,虽然weakmap的元素中有一个obj指向了obj的内存地址,但是GC不认为该指向有效,obj被视为垃圾对象

三、Proxy与Reflect

3.1 Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

Proxy的优点

1、Proxy可以直接监听数组的变化

2、proxy可以监听对象而非属性,它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,Proxy直接可以劫持整个对象,并返回一个新对象

在proxy出现前,对对象进行拦截监听可以使用Object.defineProperty,但是该APi存在一些弊端

1、无法监听数组的变化

2、只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。

3、如果是新增属性,没办法进行响应式设置,(vue2中可以通过$set方法)

3.2 Proxy基本使用

1、创建proxy实例

var proxy = new Proxy(target, handler)

其中,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截操作

如果handler没有设置任何拦截,那就等同于直接通向原对象

2、get方法

get方法用于拦截某个属性的读取操作

        let person = {
            name: "张三"
        }
        var proxy = new Proxy (person, {
            get: function(target, key) {
                if(property in target) {
                    return target[key]
                } else {
                    throw new ReferenceError("出错啦")
                }
            }
        })
        // console.log(proxy.name)
        // console.log(proxy.age) //报错

如果没有这个拦截器,访问不存在的属性只会返回undefined

3、set方法

set方法用于拦截某个属性的赋值操作

//假定obj对象有一个age属性,要求该属性是一个不大于150的整数,可以使用proxy对象保证age的属性符合要求

        handler = {
            set: function(obj, key, value) {
                if (key ==='age') { //如果属性名为age
                    if (!Number.isInteger(value)) { //如果值不为整数
                        throw new TypeError('the age is not a integer')
                    }
                    if (value > 150) { // 如果值大于150
                        throw new RangeError('the age seems invalid')
                    }
                }
                obj[key] = value
            }
        }
        const proxy = new Proxy({}, handler)
        console.log(proxy.age = 100); //100
        console.log(proxy.age = 151); //报错 the age seems invalid

4、has方法

has方法用来拦截HasProperty操作,即判断对象是否具有某个属性,这个方法会生效

let handler = {
    has (target, key) {
        if (key[0] === '_') {
            return false
        }
        return key in target
    }
}

let target = {
    _prop : 'foo', //不允许被in运算符发现
    prop : 'foo'
}

let proxy = new Proxy(target, handler)
console.log('prop' in proxy);
console.log('_prop' in proxy);

5、deleteProperty方法

该方法用于拦截delete操作,如果这个方法排抛出错误或者返回false,当前属性就无法被delete命令删除

let handler = {
    deleteProperty (target, key) {
        invariant (key, 'delete')
        return true
    }
}

let target = {
    _prop : 'foo'
}
let proxy = new Proxy(target, handler)
console.log(delete proxy._prop);

注:proxy一共有13中捕获器,不过最长用的就是以上四种

3.3Reflect

Reflect是ES6新增的一个API,字面的意思是反射,它是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的

它主要提供了很多操作JavaScript对象的方法,在某种程度上跟Object构造函数相同

既然有了Object,为什么要有Reflect呢?

Obejct作为一个构造函数,有一些操作放在它身上并不合适。所以ES6增加了Reflect对象,让一些操作都集中到了这个对象上

const obj = {
    name: 'linming'
}

//创建Proxy
const objProxy = new Proxy(obj, {
    get(target, key, receiver) {
        return target[key]
    },
    set(target, key, newValue, receiver) {
        target[key] = newValue
    }
})

我们利用Proxy本意就是不想直接操作原对象,上方的写法直接就是操作对象了

//利用Reflect重写Proxy
const objProxy = new Proxy(obj, {
    get(target, key, receiver) {
        console.log('get操作')
        return Reflect.get(target, key)
    },
    set(target, key, newValue, receiver) {
        console.log('set操作')
        Reflect.set(target, key, newValue)
    }
})

objProxy.name = 'lin' // set操作
console.log(obj.name); //lin

另外,Reflect有返回值

const Boolean = Reflect.get(target, key) //Boolean反映了获取值成功与否

const Boolean = Reflect.set(target, key, newValue) //Boolean反映了设置值成功与否

目前Reflect最多的应用场景就是跟Proxy配合使用

3.4 Reflect的常见方法

Reflect中的常见方法与Proxy是一一对应的,也是13个

方法 说明
Reflect.getPrototypeOf(target) 类似于 Object.getPrototypeOf()
Reflect.setPrototypeOf(target, prototype) 设置对象原型的函数. 返回一个 Boolean
Reflect.isExtensible(target) 类似于 Object.isExtensible()
Reflect.preventExtensions(target) 类似于 Object.preventExtensions()
Reflect.getOwnPropertyDescriptor(target, propertyKey) 类似于 Object.getOwnPropertyDescriptor()
Reflect.defineProperty(target, propertyKey, attributes) 和 Object.defineProperty() 类似,如果设置成功就会返回 true
Reflect.ownKeys(target) 返回一个包含所有自身属性(不包含继承属性)的数组
Reflect.has(target, propertyKey) 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同
Reflect.get(target, propertyKey[, receiver]) 获取对象身上某个属性的值
Reflect.set(target, propertyKey, value[, receiver]) 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true
Reflect.deleteProperty(target, propertyKey) 作为函数的delete操作符,相当于执行 delete target[name]
Reflect.apply(target, thisArgument, argumentsList) 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和Function.prototype.apply() 功能类似
Reflect.construct(target, argumentsList[, newTarget]) 对构造函数进行 new 操作,相当于执行 new target(...args)

3.5 响应式原理

proxy被应用在vue3的响应式原理中,这里简单说明一下。更详细的实现过程请参考:手写miniVue

// 下文的最终实现效果

const info = reactive({ name: "linming", counter: 10 });
const foo = reactive({ height: 1.88 });

//effect1
watchEffect(function () {
  console.log("effect1:", info.counter * 2, info.name);
});
//effect2
watchEffect(function () {
  console.log("effect2:", info.counter * info.counter);
});
//effect3
watchEffect(function () {
  console.log("effect3:", info.counter + 10, info.name);
});
//effect4
watchEffect(function () {
  console.log("effect4:", foo.height);
});

//数据发生改变, 依赖于counter的effect将被再次调用
info.counter++;

1、什么是响应式?

响应式是指当数据发生变化时,系统可以自动更新相关的视图或其他依赖项

let num = 100

//一段代码
console.log(num)
console.log(num * 2)
console.log(num ** 2)
console.log("hello world")

当num变化时,这么一段代码可以自动执行一次。那么就可以说,这段代码是可以自动响应num的变化的

2、封装一个响应式函数

如果一段代码、一些函数对某一个数值有依赖时,将其添加进一个数组(reactiveFns)中

let reactiveFns = []

function watchFn(fn) {
    reactiveFns.push(fn)
}

假如有obj对象,有两个函数foo、bar对obj中的属性有依赖

const obj = {
    name: 'lin'
}

//收集依赖
watchFn(function foo() {
    console.log("name值变化了:", obj.name)
    console.log("hello~") //100行代码
})

watchFn(function bar() {
    console.log("name值变化了:", obj.name) //10行代码
})

这样依赖,数组reactiveFns中就有foo和bar两个函数了

当obj的name发生变化时,只要遍历出数组中的函数,并进行调用便实现了响应式的效果

obj.name = "linming"
reactiveFns.forEach((item) => {
    item()
})

小总结

上面的代码中,实现了:

  • 1、将对name有依赖的函数收集到了一个数组中

  • 2、当name值变化时,遍历出数组中的函数并执行

但是,仍然不是我们想要的响应式。需要进行一些改进:

  • 1、开发中,有很多的数据(data),以及对各个数据(data)有依赖的函数代码。不能将所有的代码都装进一个reactiveFns数组中。而是需要一种数据结构,来存储不同数据的依赖

  • 2、实现依赖的自动收集,而不是手动收集;数据(data)变化时,自动执行依赖,而不是手动执行

3、创建depend类管理依赖

为了解决所有的依赖都装进一个数组中的弊端,做到一个数据有其独有的数组,我们可以创建一个depend类

// 发布订阅模式
class depend {
    constructor() {
        // 调度中心
        this.reactiveFns = [] //set结构更佳
    }

    //添加依赖
    addDepend(fn) {
        this.reactiveFns.push(fn)
    }

    //通知执行
    notify() {
        this.reactiveFns.forEach(item => {
            item()
        })
    }
}
const depend = new Depend()

function watchFn(fn) {
    // 订阅
    depend.addDepend(fn)
}

//利用watchFn找到数据name的依赖函数foo、bar,添加进调度中心
watchFn(foo)
watchFn(bar)

//当name发生变化时,通知reactiveFns执行
depend.notify()

4、自动监听对象变化,执行依赖

这一步要解决的就是每次手动执行depend.notify()的弊端

是时候用到Proxy了,利用Proxy对obj对象进行代理

const objProxy = new Proxy(obj, {
    set(target, key, newValue) {
        target[key] = newValue
        depend.notify() // obj变化时。触发更新
    }
})

当前较为完整的代码如下

class Depend {
  constructor() {
    this.reactiveFn = [];
  }
  addDepend(fn) {
    this.reactiveFn.push(fn);
  }
  notify() {
    this.reactiveFn.forEach((item) => {
      item();
    });
  }
}

const depend = new Depend();

//收集依赖的函数
function watchFn(fn) {
  depend.addDepend(fn);
}
const obj = {
  name: "linming",
};
//代理
const objProxy = new Proxy(obj, {
  get(target, key) {
    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    depend.notify(); //弊端,执行的是所有data的依赖
  },
});

// 订阅
watchFn(function foo() {
  console.log("foo,obj的name变化了:", objProxy.name);
});
watchFn(function bar() {
  console.log("bar,obj的name变化了:", objProxy.name);
});

//改变数值
objProxy.name = "lin";

上边的代码依旧没能解决多个数据依赖的保存

正常来说一个数据,应该有独立的调度中心来进行保存

const obj1 = { name: 'xiaoming', age: 24 } //有很多函数分别依赖于name和age
const obj2 = { height: 200 } //有很多函数依赖于height

设想一下,如果全部存在一个Depend实例中,那么当其中一个发生变化时,其他不想管的函数也被调用了,显然不太合理

那么我们就需要一种数据结构,能将不同的依赖函数进行整理归类。map数据结构就很适合

5、封装获取depend的函数

const weakMap = new WeakMap()
function getDepend(target, key) {
    //取出map
    let map = weakMap.get(target)
    //如果不存在则创建map
    if(!map) {
        map = new Map()
        weakMap.set(target, map) //添加
    }

    //根据key获取depend对象
    let depend = map.get(key)
    if(!depend) {
        depend = new Depend()
        map.set(key, depend)
    }
    return depend
}

一个数据应该对应一个depend

const objProxy = new Proxy(obj, {
    // ...
    set(target, key, newValue) {
        target[key] = newValue
        //最好是能够根据这个obj对象,及其key:name,获取到其依赖收集的类
        const dep = getDepend(target, key)
        dep.notify()
    }
})

通过以上代码,我们实现了对数据变更时能够自动执行依赖函数的调用

6、自动收集依赖

如何自动实现收集依赖呢?让watchFn在调用时,传入的fn参数调用一次

当fn参数被调用时,必然触发proxy的get,在get操作中进行依赖收集

//收集依赖的函数
function watchFn(fn) {
    fn() //调用,触发get
}

watchFn(function foo() {
    console.log("foo,obj的name变化了:" , objProxy.name);  //让其自动执行了一次
})

触发了get,在get中进行依赖收集。那么如何获取到要收集的那个函数呢

const objProxy = new Proxy(obj, {
    get(target, key) {
        //创建只属于target[key]的dep实例
        const dep = getDepend(target, key)
        //添加依赖(需要添加依赖的那个函数)
        dep.addDepend()
        return target[key]
    },
})

我们需要想办法获取传入到watchFn收集依赖函数中的参数,让这个参数传递给get函数中,方便添加到依赖中

let activeReactiveFn = null //全局变量
function watchFn(fn) {
    activeReactiveFn = fn
    fn() //调用,触发get
    activeReactiveFn = null
}

解决方案:让watchFn函数给全局变量activeReactiveFn赋值为它的参数,这样子我们就拿到了这个函数

const objProxy = new Proxy(obj, {
    get(target, key) {
        //创建只属于target[key]的dep实例
        const dep = getDepend(target, key)
        //添加依赖
        dep.addDepend()
        return target[key]
    },
})

// 调整Depend依赖收集的方式
class Depend
Depend {
  //...
  addDepend() {
      if(activeReactiveFn) {
          this.reactiveFn.push(activeReactiveFn); 
      }
  }
  //...
}

8、优化

1、、可能会存在同一个函数被多次收集的现象,最好把存储依赖的结构改为Set结构

class Depend {
    //订阅者
    constructor() {
        this.reactiveFn = new Set()
    }
}

2、假设我们现在又有一个obj2对象,想要变成响应式,难道要去重新创建一个proxy对象吗?能否将proxy对象变得更加通用写

function reactive(obj) {
    return new Proxy(obj, {
    get(target, key) {
        const dep = getDepend(target, key)
        dep.addDepend()
        return target[key]
    },
    set(target, key, newValue) {
        target[key] = newValue
        const dep = getDepend(target, key)
        dep.notify() //弊端,执行的是所有data的依赖
    }
}

这样一来以后调用reactive函数,就可以将传入的obj对象变成proxy对象,并在proxy对象中实现对其的get、set操作

const obj2 = {
    name: 'ming'
}
reactive(o)

附上较为完整的代码

class Dep {
  constructor() {
    // 调度中心
    this.subscribers = new Set();
  }

  // 订阅依赖
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect);
    }
  }

  // 发布
  notify() {
    this.subscribers.forEach((effect) => {
      effect();
    });
  }
}

let activeEffect = null;
function watchEffect(effect) {
  activeEffect = effect;
  effect(); //获取数据时,利用get收集依赖
  activeEffect = null;
}

//getDate工具函数
const targetMap = new WeakMap();

function getDep(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep);
  }
  return dep;
}

// vue3的数据劫持
function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const dep = getDep(target, key);
      dep.depend();
      return target[key];
    },
    set(target, key, newValue) {
      const dep = getDep(target, key);
      target[key] = newValue;
      dep.notify();
    },
  });
}

四、iterator和generator

4.1 迭代器Iterator

基本概念

迭代器和生成器将迭代的概念直接带入核心语言,并提供了一种机制来自定义 for...of 循环的行为

在js中,迭代器是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol),而迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式,在js中这个标准就是next方法

next函数

next函数是一个无参数函数,返回一个应当拥有以下两个属性的对象:

1、done(boolean)

如果迭代器可以产生序列中的下一个值,则为false;如果迭代器已经将序列迭代完毕,则为true

迭代器返回的任何JavaScript值,done为true时一般为undefined

//创建迭代器
const names = ['xiaolin', 'xiaohong', 'xiaoqiang', 'xiaohua']

let index = 0
const namesIterator = {
    next: function() {
        if(index < names.length) {
            return { done: false, value: names[index++] } //闭包
        } else {
            return { done: true, value: undefined }
        }
    }
}

console.log(namesIterator.next()); //{ done: false, value: 'xiaolin' }
console.log(namesIterator.next()); 
console.log(namesIterator.next());
console.log(namesIterator.next());  //{ done: false, value: 'xiaohua' }
console.log(namesIterator.next()); // { done: true, value: undefined }

生成迭代器的函数

function createIterator(arr) {
    let index = 0
    return {
        next: function() {
            if(index < arr.length) {
                return { done: false, value: arr[index++] }
            } else {
                return { done: true, value: undefined }
            }
        }
    }
}

测试

const names = ['xiaolin', 'xiaohong']

const namesIterator = createIterator(names)

console.log(namesIterator.next());
console.log(namesIterator.next());    
console.log(namesIterator.next());

4.2 可迭代对象Iterable

明确:迭代器对象和可迭代对象不是同一个概念,可迭代对象里有一个【Symbol.iterator】属性,可以返回一个迭代器对象

当一个对象实现了iterator protocol协议时,它就是一个可迭代对象

这个对象要求必须实现了@@iterator方法,在代码中我们使用Symbol.iterator访问该属性

优点:当一个对象变成一个可迭代对象时,进行某些迭代操作,比如for...of操作时,其实就会调用它的@@iterator方法

//实现一个可迭代对象
const iterableObj = {
    names: ['abc', 'cba', 'nba'],
    [Symbol.iterator]: function() {
        let index = 0
        return {
            next: () => {
                if (index < this.names.length) {
                    return { done: false, value: this.names[index++] }
                } else {
                    return { done: true, value: undefined }
                }
            }
        }
    }
}
//必须使用箭头函数,才能保证this指向的是上层作用域interableObj对象

上方的iterableObj就是一个可迭代对象

//1.第一次调用
const iterator = iterableObj[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())

//第二次调用
const iterator2 = iterableObj[Symbol.iterator]()
console.log(iterator2.next())
console.log(iterator2.next())

可迭代对象每次调用都会生成一个新的迭代器,还能使对象可以被for...of遍历

for(let value of obj) // 报错:TypeError: obj is not iterable

for(let value of iterableObj) //输出:'abc', 'cba', 'nba'

所以for...of的原理是遍历出其中的value值

原生可迭代对象

我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象:

String、Array、Map、Set、arguments对象、NodeList集合

//测试
const arr = [1, 2, 3]
console.log(arr[Symbol.iterator]) //输出:[Function: values]

const arrIterator = arr[Symbol.iterator]()
console.log(arrIterator.next())
console.log(arrIterator.next())
console.log(arrIterator.next())
console.log(arrIterator.next())

可迭代对象的应用

1、除了for...of外,展开语法、yield、解构赋值也都用到了可迭代对象

2、创建一些对象时,要求我们传入的必须是可迭代对象:new Map([Iterable]) new Set([Iterable])

3、一些方法的调用,也要求参数是可迭代的

如:Promise.all(iterable) Promise.race(iterable) Array.from(iterable)

4.3 生成器Generator

生成器函数是ES6中新增的一种函数控制、使用方案,它可以让我们更加灵活地控制函数什么时候继续执行、暂停执行

function foo () {
    代码块1
    //需求:暂停执行
    代码块2

    代码块3
}

有时候我们有这种需求:暂停执行一个函数,稍后恢复执行(不能使用return)

事实上,生成器是一种特殊的迭代器

而生成器函数是一个函数,但是和普通函数有一些区别:

1、生成器函数需要在function后加上一个符号*

2、生成器函数可以通过yield关键字来控制函数的执行流程

3、生成器函数的返回值是一个Generator(生成器)

注意:生成器函数与生成器是不同的,在调用生成器函数可以返回一个生成器

基本使用

function* foo() {
    console.log('生成器函数开始执行~')

    const value1 = '100'
    console.log('第一段代码', value1)
    yield

    const value2 = '200'
    console.log('第二段代码', value2)
    yield

    const value3 = '300'
    console.log('第三段代码', value3)
    console.log("生成器函数结束")
}

//不能直接调用
foo()

//返回值是一个generator对象
const generator = foo()
generator.next() //生成器函数开始执行~ 第一段代码 100
generator.next() //第二段代码 200
generator.next() //第三段代码 300 生成器函数结束

返回值

如果想要每一小段有返回值,可以在yield后边跟上返回值

function* foo() {
    console.log('生成器函数开始执行~')

    const value1 = '100'
    console.log('第一段代码', value1)
    yield '返回值'
    //....
}
const generator = foo()
console.log(generator.next());
//生成器函数开始执行~
//第一段代码 100
//{ value: '返回值', done: false }

参数

如果想要每块代码有独有的参数,例如第一段代码有自己的参数m,第二段代码有自己的参数n....

//第二段代码的参数,是第一段代码yield的返回值
function* foo() {
    console.log('生成器函数开始执行~')

    const value1 = 100
    console.log('第一段代码', value1)
    const m = yield value1

    const value2 = 200 * m
    console.log('第二段代码', value2)
    yield value2

    const value3 = 300
    console.log('第三段代码', value3)
}


const generator = foo()
console.log(generator.next());
console.log(generator.next(20)); // 第二段代码 4000  { value: 4000, done: false }

如果是想要给第一段代码传进一些参数

function* foo(x) {
    console.log('生成器函数开始执行~')

    const value1 = 100 * x
    console.log('第一段代码', value1)
    yield value1
    //....
}

const generator = foo(33)
console.log(generator.next());
// 生成器函数开始执行~
// 第一段代码 3300
// { value: 3300, done: false }

终止执行

console.log(generator.return(10));

4.4 生成器替代迭代器

之前我们写一个生成迭代器的函数,如下

function createIterator(arr) {
    let index = 0
    return {
        next: function() {
            if(index < arr.length) {
                return { done: false, value: arr[index++] }
            } else {
                return { done: true, value:  undefined }
            }
        }
    }
}
const arr = [1,2,4,5]
const iterator = createIterator(arr)
console.log(iterator.next());

其实生成器是一种特殊迭代器,调用生成器函数返回一个生成器,生成器约等于迭代器,可以调用next方法

function* createIterator(arr) {
    let index = 0
    yield arr[index++] //返回生成器
    yield arr[index++]
    yield arr[index++]
    yield arr[index++]
}
const arr = [1,2,4]
const generator = createIterator(arr)
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());


//甚至可以更加简洁
function* createIterator(arr) {
    for(const item fo arr) {
        yield item
    }
}

//最简洁的方式(语法糖)
function* createIterator(arr) {
    yield* arr
}

五、async和await

5.1 简单推导

假设有这么一个网络请求函数request,每次请求到结果后需要处理一下,再次发送网络请求

//模拟网络请求
function request(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(url);
    }, 2000);
  });
}

方式一:回调地狱

request('mall').then(res =>{
    request(res + '/home').then(res => {
        request(res + '/detail').then(res => {
            console.log(res)
        })
    })
})
//输出:mall/home/detail

方式二:promise返回值

request('mall').then(res => {
    return request(res + '/home')
}).then(res => {
    return request(res + '/detail')
}).then(res => {
    console.log(res)
})
//输出:mall/home/detail

方式三:promise + generator

function *getData() {
    yield request('mall')
}

const  generator = getData()
generator.next()
//输出:request('mall')结果(一个promise):{ value: Promise { <pending> }, done: false }

generator.next().value.then(res1 => {
    console.log(res1)
}) //输出:mall

重要一步:(理解yield的参数传递)将拿到的mall,传到第一个yield前面const res1 = yield request('mall')

调用第二个yield时,就可以使用res1了

function *getData() {
    cosnt res1 = yield request('mall')
    const res2 = yield request(res1 + '/home')
}
const  generator = getData()
generator.next().value.then(res1 => {
    generator.next(res1) //传递参数
})

完整的代码如下

function *getData() {
    const res1 = yield request('mall')
    const res2 = yield request(res1 + '/home')
    const res3 = yield request(res2 + '/detail')
}
const  generator = getData()
generator.next().value.then(res1 => {
    generator.next(res1).value.then(res2 => {
        generator.next(res2).value.then(res3 => {
            console.log(res3)
        })
    })
})

//输出:mall/home/detail

是不是觉得上边的代码相比起前两种方式没什么改进?但是其实可以利用一些方式让生成器自调用

或者利用一些库(co),来省略这一步。所以经过这两种方式的转化,代码简化为如下

function *getData() {
    const res1 = yield request('mall')
    const res2 = yield request(res1 + '/home')
    const res3 = yield request(res2 + '/detail')
    console.log(res3)
}

方式四:async与await

而async与await就是上方的语法糖(* -> async,yield -> await )

async function getData() {
    const res1 = await request('mall')
    const res2 = await request(res1 + '/home')
    const res3 = await request(res2 + '/detail')
    console.log(res3)
}
getData()
//输出:mall/home/detail

5.2 基本语法

特点

1、async和await是用来处理异步函数的

2、promise通过then链来解决多层回调问题,async和await可以进一步优化它

3、async是“异步”的简写,await是“async wait”的简写

4、async用于声明一个function是异步的,await用于等待一个异步方法执行完成的

5、await只能出现在async的函数中

async基本使用

async的用法很简单,在函数前面加上这一关键字,按着平时使用函数的方式去使用它

我们来对比一下普通函数与async的区别

    // 普通函数声明
    function timeout1 () {
        return 'hello wolrd'
    };

    // async函数声明
    async function timeout2 () {
        return 'hello world'
    }

    // 两者的结果
    console.log(timeout1());  //输出:hello world
    console.log(timeout2());  //输出:promise对象

async返回一个promise对象,如果要获取结果,就可以使用then方法了

    timeout2().then(res => {
        console.log(res);
    }).catch(err => {
        console.log(err);
    })

值得注意的是:async函数返回一个promise对象,如果在函数return一个直接量,async会把这个直接通过promise.resolve()封装成Promise对象。如果函数内部出错了,则调用promise.reject,也是返回一个promise对象

async function timeout(flag) {
    if (flag) {
        return 'hello world'
    } else {
        throw 'my god, failure'
    }
}
console.log(timeout(true))  // 调用Promise.resolve() 返回promise 对象。
console.log(timeout(false)); // 调用Promise.reject() 返回promise 对象。

await基本使用

await后边可以放任何表达式,不过放的更多的返回一个promise对象的表达式。

现在写一个函数,让它返回promise 对象,该函数的作用是2s 之后让数值乘以2

function mult(num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * 2);
    }, 2000);
  });
}

async function getResult(num) {
  const res = await mult(num);
  console.log(res);
}

getResult(44); //2s后输出88

执行过程:调用了getResult函数,它里边遇到了await,代码暂停到了这里不再向下执行。只有等到它后边的promise对象执行完毕,拿到promise resolve的值并进行返回,它才继续向下执行

注:

1、如果await等到的不是一个promise对象,那么await表达式的运算结果就是它等到的东西;

2、如果等到了promise,它会阻塞后边的代码,等着promise对象resolve,然后得到resolve的值,作为await表达式的运算结果

5.3 异步函数传染性

在开发中,如果定义一个网络请求函数并要求返回其结果,那么需要使用到async await。并且如果其他函数执行了该函数,他们也需要用上async await

async function getData() {
  return await fetch('https://my-json-server.typicode.com/typicode/demo/profile').then((resp) => resp.json())
}

async function fn1(){
  //other  works
  return await getData()
}

async function fn2(){
  //other  works
  return await fn1()
}
fn2()

弊端:每个使用到getData的函数,都需要使用到async await。破坏了纯函数的结构,产生了副作用

想要实现的效果,不使用async await能够获取到网络请求的结果

思路:在调用时直接报错,在报错中进行结果的处理

//实现
function run(func) {
  let cache = []; //缓存的列表,由于可能不止一个fetch,所以要用一个list
  let i = 0; //缓存列表的下标
  const _originalFetch = window.fetch; //储存原先的fetch
  window.fetch = (...args) => {
    //重写fetch函数,这个fetch要么抛出异常,要么返回真实的数据
    if (cache[i]) {
      //判断一下缓存是否存在,如果存在就返回真实的数据或抛出异常
      if (cache[i].status === "fulfilled") {
        return cache[i].data;
      } else if (cache[i].status === "rejected") {
        throw cache[i].err;
      }
    }
    const result = {
      status: "pending",
      data: null,
      err: null,
    };
    cache[i++] = result; //添加缓存
    //发送请求
    //真实的fetch调用
    const prom = _originalFetch(...args)
      .then((resp) => resp.json())
      .then(
        (resp) => {
          //等待返回结果,然后修改缓存
          result.status = "fulfilled";
          result.data = resp;
        },
        (err) => {
          result.status = "rejected";
          result.data = err;
        }
      );
    //如果没有缓存,就添加缓存和抛出异常
    throw prom;
    //这里为什么会抛出真实fetch返回的promise,主要是因为外面会用到这个promise然后等待拿到最终结果
  };

  try {
    //在try里调用func也就是上述的main函数
    //由于main里面有fetch,且第一次没有缓存,所以会抛出一个异常
    func();
  } catch (err) {
    //从这里捕获到异常
    //这里的err就是上述fetch返回的promise

    if (err instanceof Promise) {
      //验证一下是不是promise
      const reRun = () => {
        i = 0; //重置一下下标
        func();
      };
      err.then(reRun, reRun); //待promise返回结果后重新执行func,也就是重新执行main
      //这次执行已经有缓存了,并且返回中有了正确的结果,所以重写的fetch会返回真实的数据
    }
  }
}


// 调用
function main() {
  const res = fetch(
    "https://my-json-server.typicode.com/typicode/demo/profile"
  );
  console.log("res", res); //res要得到一个data数据而不是一个promise对象
}
run(main);

6、其他

6.1 禁止网页被调试

1、如何绕过别人网站的断点进行调试

  • 禁止网页所有断点
  • 右键断点选择add logpoint,返回false
  • 右键断点选择add script to ignore list,添加的忽略代码

2、如何防止自己的网站被调试

;(() => {
  function block() {
    if (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200) {
      document.body.innerHTML = '检测到非法调试,请关闭后刷新重试!'
    }
    setInterval(() => {
      ;(function () {
        return false
      })
        ['constructor']('debugger')
        ['call']()
    }, 50)
  }
  try {
    block()
  } catch (err) {}
})()

6.2 indexedDB

indexedDB是一种数据库。一般而言,存储大量数据是需要使用后端的数据库的,但是如果想要保存在浏览器,就可以选择使用indexedDB

indexedDB是一种底层的API,用于在客户端存储大量的结构化数据。它是一种事务型数据库系统,是一种基于JavaScript面向对象数据库。我们需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务

简单示例

1、打开indexedDB的某一个数据库

通过indexedDB.open(数据库名称,数据库版本)方法打开一个数据库(如果数据库不存在则创建)

const dbRequest = indexedDB.open("dbName", 2)

打开成功/失败会触发一下回调函数

//失败
dbRequest.onerror = function(err) {
    console.log("打开数据库失败~")
}
//成功
let db = null
dbRequest.onsuccess = function(event) {
    console.log("打开数据库成功~")
    db = event.target.result
}

2、第一次打开数据库时,触发以下回调

dbRequest.onupgradeneeded = function(event) {
    const db = event.target.result
    //创建一些存储对象,users表,主键为keyPath
    db.createObjectStore("users", { keyPath: "id" })
}

后续可以通过db这个对象操作数据库

3、往表中添加数据

const  info = {
    "id" : 200,
    "name" : "xiaoming",
    "age" : "14"
}

setTimeout(() => {
    const transaction = db.transaction("users", "readwrite")
    let store = transaction.objectStore("users")
    // 往users表中新增数据  
    const res = store.add(info)
    res.onsuccess = function() {
        console.log("添加操作完成");
    }
}, 300)

4、查询数据

const request = store.get(200)
request.onsuccess = function(event) {
    console.log(event.target.result)
}

//输出:{id: 200, name: 'xiaoming', age: '14'}
评论
  • 支持 Markdown 格式
  • 评论需要登录
  • 邮箱会回复提醒(也许会在垃圾箱内)
0 /400 字