跳到主要内容

24 篇博文 含有标签「JavaScript」

查看所有标签

· 阅读需 5 分钟

在集合A和集合B中,属于集合A,同时也属于集合B的元素组成的集合,就是交集

在A中所有不属于集合B元素,组合成集合,就是差集

那么在平时的开发中,如何使用差集和交集来解决问题呢?

现在有这样的场景,在一个表格编辑数据后,要把编辑前的数据和修改后的数据,汇总。

源数据为:

const arr1 = [{ name: 11, id: 1 }, { name: 21, id: 2 }, { name: 31, id: 3 }, { name: 41, id: 4 }, { name: 51, id: 5 }, { name: 61, id: 6 }];

在页面中表现为:

在这里插入图片描述

现在删除第一行数据,第二行的名字改为2109,第三行的名字改为3321;然后新增两行,分别为:71、81。

数据如下:

const arr2 = [{ name: 2109, id: 2 }, { name: 3321, id: 3 }, { name: 41, id: 4 }, { name: 51, id: 5 }, { name: 61, id: 6 }, { name: 71, id: null }, { name: 81, id: null }];

页面为:

在这里插入图片描述 由于是新增数据还没有提交保存,所以对应的序号,也就是ID为空。

最终想要的效果图下图所示:

在这里插入图片描述 需要在表格中体现那些数据是修改、删除、新增,哪些数据没有改变。

思路:

  1. 源数据是一个数组arr1;
  2. 修改后的数据也是一个数组arr2;
  3. 删除的数据,在数组arr1中有,数组arr2中没有;
  4. 修改的数据,在数组arr1和arr2中,都找对应的ID;
  5. 新增的数据,只出现在数组arr2中。

那么数组arr2与数组arr1的差集,就是新增的数据:

let add = arr2.filter(x => arr1.every(y => y.id != x.id))

数组arr1与数组arr2的差集,就是删除的数据:

let del = arr1.filter(x => arr2.every(y => y.id != x.id))

修改或者没有修改数据,就是数组arr1和数组arr2的交集:

// arr1、arr2的交集
let arr12Inter = arr1.filter(x => arr2.some(y => x.id === y.id))
let arr21Inter = arr2.filter(x => arr1.some(y => x.id === y.id))

最后一步,就是组合所有的差集、交集,汇总成新的数组:

for (let index = 0; index < arr12Inter.length; index++) {
newArr.push({ oldData: arr21Inter[index], newData: arr12Inter[index] })
}

del.forEach(item => newArr.push({ oldData: item, newData: null }))
add.forEach(item => newArr.push({ oldData: null, newData: item }))

完整代码:

const arr1 = [{ name: 11, id: 1 }, { name: 21, id: 2 }, { name: 31, id: 3 }, { name: 41, id: 4 }, { name: 51, id: 5 }, { name: 61, id: 6 }];

const arr2 = [{ name: 2109, id: 2 }, { name: 3321, id: 3 }, { name: 41, id: 4 }, { name: 51, id: 5 }, { name: 61, id: 6 }, { name: 71, id: null }, { name: 81, id: null }];
let newArr = [];
// arr1——>arr2的差集:删除
let del = arr1.filter(x => arr2.every(y => y.id != x.id))

// arr2——>arr1的差集:新增
let add = arr2.filter(x => arr1.every(y => y.id != x.id))

// arr1、arr2的交集:修改
let arr12Inter = arr1.filter(x => arr2.some(y => x.id === y.id))
let arr21Inter = arr2.filter(x => arr1.some(y => x.id === y.id))

console.log("arr1与arr2的差集:", del)
console.log("arr2与arr1的差集:", add)
console.log("交集", arr12Inter, arr21Inter)

for (let index = 0; index < arr12Inter.length; index++) {
newArr.push({ oldData: arr21Inter[index], newData: arr12Inter[index] })
}

del.forEach(item => newArr.push({ oldData: item, newData: null }))
add.forEach(item => newArr.push({ oldData: null, newData: item }))

console.log("汇总:", newArr)

在这里插入图片描述

使用交集、差集,仅仅是一种方式!

· 阅读需 3 分钟

常用的实例函数和静态函数

引用数据类型,是通过 new 操作符来生成的,然而 new 在执行的时候,会改变 this 的指向。

执行 new 的时候,首先是创建一个空对象,把空对象的proto属性指向构造函数的 prototype 属性,完成了将构造函数的 this 指向新建的这个空对象。

Object 类型中,有这样的几个实例函数:

  1. hasOwnProperty(propertyName)函数,判断了一个属性是不是实例自己定义的;但是它不会检查原型链上的属性。
  2. propertyIsEnumerable(propertyName)函数,判断属性是否是实例属性并且该属性是否可枚举;

Object 类型的静态函数:

  1. Object.create(),主要是用来创建并且返回一个指定原型和指定属性的对象;
  2. Object.defineProperties()函数,添加或者修改对象属性的值,第一个参数是操作对象,第二个参数是添加或者修改的属性信息;
  3. Object.getOwnPropertyNames()函数,获取对象的所有的属性和函数;
  4. Object.keys()函数,获取对象可枚举的实例属性;

Array 类型

###判断一个数组是否为空的方法

  1. 一般使用的是 instanceof运算符,是在原型链上查找变量是否是某个类型的实例, 比如:
const str = [1,2];
console.log(str instanceof Array);// true
console.log(str instanceof Object);// true

这就导致我们不能精准判断一个变量的具体数据类型。

  1. 第二个方式使用 Array 的 isArray 方法,比如:console.log(Array.isArray(arr); ,变量是数组就返回 true,法则返回 false。

  2. 通过判断构造函数,判断变量的构造函数是 Array 类型,还是 Object 类型。因为一个对象是通过构造函数来生成的。代码如下:

const arr = [];
console.log(arr.constructor === Array)

每一个变量都会有一个proto属性,表示隐式原型,proto 属性指向了构造函数的原型。比如:console.log(arr.__proto__ === arr.constructor.prototype);//true

引用数据类型都是继承了 Object 类型,所以它们都含有了 toString 函数,不同数据累心的 toString 的返回值是不一样的,因此可以通过 toString 函数来判断一个变量是数组还是对象。结果返回的是“[object Array]”或者"[object Object]"。

比如:

const obj = {}
const arr = []

console.log(Object.prototype.toString.call(obj)); //[object Object]
console.log(Object.prototype.toString.call(arr)) // [object Array]
console.log(Object.prototype.toString.call(908)) // [object Number]
console.log(Object.prototype.toString.call("7sd")) // [object String]

· 阅读需 5 分钟

现在有这样的一个数组:

const arr = [1, 23, 4, 5, 6,3,4,5];

需要封装一个方法,去掉数组中重复的元素,需要的得到这样的目标数组:

const arr = [1, 23, 4, 5, 6,3,];

1、遍历数组

通过遍历数组来实现去重功能,那么就需要定义一个新数组来接收目标数组,在遍历原数组的时候,判断目标数组是否含有该元素,如果没有,那么就将该元素添加到目标数组中,如下代码:

function unique(array) {
var result = [];
for (var i = 0; i < array.length; i++) {
if (result.indexOf(array[i]) === -1) {
result.push(array[i]);
}
}
return result;
}
console.log(unique(arr)); // [ 1, 23, 4, 5, 6, 3 ]

这个方法思路简单,但是需要定义新的变量。

2、key-value

对方法1的函数进行一个延伸,就是新建一个对象和数组,遍历数组的时候,判断数组元素时候是对象的属性,如果不是,则将该元素添加到目标数组,并且将该元素作为对象的新属性;如果不是,则不对该元素进行处理。其中需要对元素做一个类型判断,否则会出现Number和String的冲突,代码如下:

function unique2(array) {
var obj = {}, result = [], val, type;
for (var i = 0; i < array.length; i++) {
val = array[i];
type = typeof val;
if (!obj[val]) {
obj[val] = [type];
result.push(val);
} else if (obj[val].indexOf(type) < 0) { // 判断数据类型是否存在
obj[val].push(type);
result.push(val);
}
}
return result;
}
console.log(unique2(arr)); //[ 1, 23, 4, 5,6, 3, '5']

3、排序去重

通过原生的sort函数,对原数组进行排序,然后对排序后的数组进行一个相邻元素的去重,代码如下:

function unique3(array) {
var result = [array[0]];
array.sort(function (a, b) { return a - b });
for (var i = 0; i < array.length; i++) {
if (array[i] !== result[result.length - 1]) {
result.push(array[i]);
}
}
return result;
}
console.log(unique3(arr)); //[1, 3, 4, 5,'5', 6, 23]

需要定义一个目标数组。

4、双层循环数组

只是这种方式,一个数组需要遍历两次,代码如下:

function unique4(array) {
var result = [];
for (var i = 0, l = array.length; i < array.length; i++) {
for (var j = i + 1; j < l; j++) {
// 依次与后面的值进行比较,如果出现相同的值,则更改索引值
if (array[i] === array[j]) {
j = ++i;
}
}
// 每轮比较完毕后,索引为i的值为数组中只出现一次的值
result.push(array[i]);
}
return result;
}
console.log(unique4(arr)); //[1, 23, 5, 6, 3, 4, '5']

5、reduce函数

通过reduce函数实现去重,也是需要定义一个对象,虽然不需要重新定义目标数组,是因为reduce函数的第二个参数,需要传入一个空数组即可, 代码如下:

function unique5(array) {
var obj = {}, type;
return array.reduce(function (preValue, curValue) {
type = typeof curValue;
if (!obj[curValue]) {
obj[curValue] = [type];
preValue.push(curValue);
} else if (obj[curValue].indexOf(type) < 0) { // 判断数据类型是否存在
obj[curValue].push(type);
preValue.push(curValue);
}
return preValue;
}, []);
}
console.log(unique5(arr)); //[1, 23, 5, 6, 3, 4, '5']

6、ES6的Set数据结构

通过Set数据结构和Array.from(),,set结构具有每个成员唯一性,本身就具有去重的功能,但是它是类数组而不是数组,所以通过Array.form函数对类数组做一个转换。

function unique6(array) {
return Array.from(new Set(array));
}
console.log(unique6(arr)); //[1, 23, 5, 6, 3, 4, '5']

· 阅读需 4 分钟

tostring 和 valueOf 函数是解决值的显示和运算的问题。所有的 Object 类型的数据都自带 toString 和 valueOf 函数。

比如我们定一个 Object 类型数据:

const obj = {
name: "duxin"
}
console.log(obj.valueOf()); // { name: 'duxin' }

toString()

toString 函数的作用是把一个逻辑转换为字符串,并且返回 Object 类型数据的 toString()默认的返回值"[object Object]"。

如果我们在定义对象的时候,可以重写 tosString 函数,这样 toString 的返回值可读性会更好一些,比如:

const obj = {
name: "duxin",
toString:function(){
return this.name;
}
}
console.log(obj.valueOf()); // { name: 'duxin' }
console.log(obj.toString()); // duxin

以下这些函数都是有自定义的 toString 函数:

  1. Array 的 toString 函数返回值是以逗号分隔的字符串;
  2. Function 的 toString 函数返回值是函数自身的文本定义;
  3. Date 的 toString 函数返回值是时间字符串;

valueOf()

valueOf 函数返回引用类型的原始值,如果没有原始值,就返回"{}",就是空对象的字面量。

  1. Array的valueOf函数返回的值数组本身;
  2. Function的valueOf函数返回的是函数本身;
  3. Date的valueOf函数返回的值时间戳。

在发生数据类型转换的时候,有liang两种场景:

  1. 引用数据类型转为String,先是调用toString函数,有值的话,就直接返回该字符串;如果对象没有toString,那就调用valueOf函数,然后将原始值转为字符串并且返回;如果toString或者valueOf都不能获取原始值,那就抛出类型转换异常;
var arr = [];

arr.toString = function () {
console.log('调用了toString()函数');
return [];
};

arr.valueOf = function () {
console.log('调用了valueOf()函数');
return [];
};

console.log(String(arr));

在执行String(arr)的时候,是先将数组转为字符串,调用的是toString,它的结果不能转为string;

那就调用valueOf函数,它的返回值也是一个空数组,也不能转为字符串,那就只能抛出类型转换异常了。

  1. 引用类型在转化为number的时候,首先会判断对象是否含有valueOf函数,如果有,那就调用valueOf函数,把它的返回值转为数字,然后返回;如果没有valueOf函数,就调用toString函数,将返回值转为数字并返回。如果toString或者valueOf都不能获取到原始值的话,那就抛出类型转换异常。

· 阅读需 6 分钟

作用域


作用域就是当前执行环境的上下文,它限制了变量、函数的有效范围。

在当前作用域下声明的变量、函数只能在当前作用域内以及它嵌套的子作用域内有效。这样避免变量和函数的命名冲突,还可以形成私有数据,从而保证数据不会被外部作用域篡改。

作用域分为全局作用域和局部作用域、块作用域。

全局作用域

全局全局作用域中定义的变量、函数,在任何地方都能访问,在JavaScript代码中最外层定义的变量、函数,都是在全局作用域中的。

局部作用域

局部作用域可以访问全局作用域或者父级作用域中的变量和函数。

变量的提升机制

在JavaScript中,函数使用var声明的变量都具有提升机制,就是先引用后声明。

这是因为JavaScript编译器会提前检查代码中的函数和var声明的变量,然后把它们提升到当前作用域的顶部,这样保证代码的正常运行,但是变量的赋值没有被提升。该变量变成了undefined。

临时隔离区

用let关键字定义的变量,不能在初始化之前访问的原因是,它的声明被放到了临时隔离区,临时隔离区会在执行块级作用域的第一行代码之前生效。在变量完成初始化之后,才被释放出来。

在变量完成初始化之前,是不能访问临时隔离区的变量。

换句话来说,临时隔离区就是存放变量初始化的过程。


闭包


函数式编程,是以函数为核心,每一个操作,通过对函数的组合和复用来形成复杂的业务逻辑。只需要调用一次函数,就完成所有的业务逻辑。

函数式编程中的闭包,指的是一种语法形式和变量查找机制,在一系列的函数嵌套中,所有内部函数都可以访问外部函数以及全局作用域中的变量和函数。

闭包有一个用处:定义私有变量,闭包外面是不能访问内部变量的,这对内部变量起到一个保护作用,调用者只能通过闭包暴露出来的函数或者是对象来对内部变量的修改。


高阶函数


高阶函数就是接收函数作为参数,最终返回一个函数,这样的函数就是高阶函数。

柯里化

柯里化就是把一个接收多个参数的函数,转化为一系列接收一个参数的子函数。这个过程就是柯里化,比如:通过汇率计算一美元等于多少人民币。

首先我们定义一个函数,接收汇率和美元数量,然后返回计算的结果,代码如下:

function usToCny(amount,rate){
return amount * rate;
}

但是每次计算的时候,都需要传入汇率,我们可以通过柯里化的方式,来简化这个函数,代码如下

function converRate(rate){
return (amount)=> amount * rate;
}

// 普通调用
converRate(6.3)(100)

// 或者是
const uToC = converRate(6.24);
console.log(uToC(100))

方法很多,柯里化仅仅是其中的一种。


函数缓存


函数缓存,就对于比较耗时的函数而言,把已经执行过的函数的结果保存下来作为缓存,如果下次再次需要这个函数的执行结果,那就先判断缓存中是否含有该结果,如果有就不用执行该函数。

一般是把函数的参数列表当作缓存的key,如果第二次调用函数传递相同的参数,就会返回缓存中的结果。

函数缓存,有效提升了代=代码程序的性能,加快相应速度。一般用于变化很少、不依赖外部条件并且很耗时的操作。比如计算斐波那契数列。

这是没有设置缓存的代码:

function fib(n){
if(n<=1)return n;
return fib(n-1)+fib(n-2);
}

const start = new Date().getTime();
console.log(fib(10))
const end = new Date().getTime();

console.log("执行花费时间:",end - start);

在这个例子中,有几个值是计算多次的,这就造成不必要的性能消耗。这就体现了函数缓存的重要性了,我们可以将已经计算过的值保存起来,,在后面的计算中,判断是否有缓存,如果有那么就是直接使用。

上面的例子修改后:

function fib(n){
if(n<=1){return n}
if(fib[n]){return fib[n]}
fib[n] = fib(n-1) + fib(n-2);
return fib(n);
}

· 阅读需 10 分钟

JavaScript中有8种数据类型,其中,7种数据类型,分别是Number、Boolean、String、Null、Undefined、Symbol、BigInt;引用数据类型为Object。

Object是结构化的类型,它是由多种数据类型组成的,另外JavaScript内置的Array、Map、WeakMap、Set、WeakSet等等都是属于Object类型的。

typeof

JavaScript提供的typeof方法,用来判断基本的数据类型,比如:

     typeof 5;                //"number"
typeof true; //"boolean"
let str="hello";
typeof str; //"string"
typeof{prop:"value"}; //"object"
typeof function(){}; //"function"

但是typeof不能用来判断基本数据类型中的null,会返回object,这是JavaScript规范中遗留的问题,这就不用赘述了,只要记住这个现象就好。

Null和Undefined

undefined表示未定义,不存在的值;null表示一个空值,表示存在某一个变量,只是该变量为空而已。

Number类型

在JavaScript中,可以使用二进制、八进制、十进制和十六进制表示数字,都是属于Number类型。使用特殊的标记来区分各种进制数:

  1. 0b开头表示二进制数;
  2. 0o开头表示八进制数;
  3. 0x开头表示十六进制。

如果数字位数很长,可以使用下划线进行数位分组,便于阅读,比如:

     1000_0000_0000     //整数
22.4211_3677_7478 //小数
0b1100_0010_1101 //二进制

JavaScript中的整数和浮点数,都是属于Number类型,统一采用64位浮点数进行存储,在进行运算的时候,会出现一些差异。

整数参与运算的时候,结果复核预期,对于浮点数参与的运算,就会出现意想不到的结果,比如0.2+0.1并不等于0.3,而是0.30000000000000004。

原因是JavaScript的浮点数在计算机中,总的长度为64位,最高位是符号位,接下来的11位是指数位,最后的52位为小数位,也就是有效数字的部分。

第0位:符号位表示正负,0表示正数,1表示负数; 第1位到第11位:存储指数部分,用e表示; 第11位到第63位:存储小数部分。如下图所示:

浮点数的表示图

对于浮点数来说,最多只能保存52位的小数位,当存在一个无限循环的小数时,只能截取前面的52位,这就导致精度丢失,所以就会出现这种现象。

浮点数参与的运算中,浮点数的小数位按照“乘以2取整,顺序排列”的方法转换为二进制。

0.1转换为二进制:

0.1*2=0.2,取出整数部分0,还剩下0.2,接着往下计算;

0.2*2=0.4,取出整数部分0,剩下0.4;

0.4*2=0.8,取出整数部分0,剩下0.8;

0.8*2=1.6,取出整数部分1,剩下0.6;

0.6*2=1.2,取出整数部分1,剩下0.2;

0.2*2=0.4,取出整数部分0,剩下0.2;

0.4*2=0.8,取出整数部分0,剩下0.8;

0.8*2=1.6,取出整数部分1,剩下0.6;

...

这样就会一直循环下去。0.2也类似。

在对浮点数进行运算的时候,需要进行处理,这是封装好的方法:

const operationObj = {
/**
* 处理传入的参数,不管传入的是数组还是以逗号分隔的参数都处理为数组
* @param args
* @returns {*}
*/
getParam(args) {
return Array.prototype.concat.apply([], args);
},

/**
* 获取每个数的乘数因子,根据小数位数计算
* 1.首先判断是否有小数点,如果没有,则返回1;
* 2.有小数点时,将小数位数的长度作为Math.pow()函数的参数进行计算
* 例如2的乘数因子为1,2.01的乘数因子为100
* @param x
* @returns {number}
*/
multiplier(x) {
let parts = x.toString().split('.');
return parts.length < 2 ? 1 : Math.pow(10, parts[1].length);
},

/**
* 获取多个数据中最大的乘数因子
* 例如1.3的乘数因子为10,2.13的乘数因子为100
* 则1.3和2.13的最大乘数因子为100
* @returns {*}
*/
correctionFactor() {
let args = Array.prototype.slice.call(arguments);
let argArr = this.getParam(args);
return argArr.reduce((accum, next) => {
let num = this.multiplier(next);
return Math.max(accum, num);
}, 1);
},

/**
* 加法运算
* @param args
* @returns {number}
*/
add(...args) {
let calArr = this.getParam(args);
// 获取参与运算值的最大乘数因子
let corrFactor = this.correctionFactor(calArr);
let sum = calArr.reduce((accum, curr) => {
// 将浮点数乘以最大乘数因子,转换为整数参与运算
return accum + Math.round(curr * corrFactor);
}, 0);
// 除以最大乘数因子
return sum / corrFactor;
},

/**
* 减法运算
* @param args
* @returns {number}
*/
subtract(...args) {
let calArr = this.getParam(args);
let corrFactor = this.correctionFactor(calArr);
let diff = calArr.reduce((accum, curr, curIndex) => {
// reduce()函数在未传入初始值时,curIndex从1开始,第一位参与运算的值需要
// 乘以最大乘数因子
if (curIndex === 1) {
return Math.round(accum * corrFactor) - Math.round(curr * corrFactor);
}
// accum作为上一次运算的结果,就无须再乘以最大因子
return Math.round(accum) - Math.round(curr * corrFactor);
});
// 除以最大乘数因子
return diff / corrFactor;
},

/**
* 乘法运算
* @param args
* @returns {*}
*/
multiply(...args) {
let calArr = this.getParam(args);
let corrFactor = this.correctionFactor(calArr);
calArr = calArr.map((item) => {
// 乘以最大乘数因子
return item * corrFactor;
});
let multi = calArr.reduce((accum, curr) => {
return Math.round(accum) * Math.round(curr);
}, 1);
// 除以最大乘数因子
return multi / Math.pow(corrFactor, calArr.length);
},

/**
* 除法运算
* @param args
* @returns {*}
*/
divide(...args) {
let calArr = this.getParam(args);
let quotient = calArr.reduce((accum, curr) => {
let corrFactor = this.correctionFactor(accum, curr);
// 同时转换为整数参与运算
return Math.round(accum * corrFactor) / Math.round(curr * corrFactor);
});
return quotient;
}
};

Symbol类型

Symbol是ES6规范中新增的一种数据类型,是一种不会重复的数据类型。可以用来作为对象属性名,这样防止后续属性名被覆盖。比如:


let obj = {};
let prop = Symbol("name");
let props = Symbol("name");
obj[prop]='name1';
obj[props]=908;

console.log(obj[prop]); // name1
console.log(obj[props]); // 908

obj对象的属性名是通过Symbol来定义的,都是传入相同的参数,结果却是两个不同的属性名。

数据类型转换

JavaScript是动态类型语言,在定义变量的时候,不用指定具体的类型,之后可以给它赋值不同数据类型的值。

但是在运算的时候,需要对应的数据类型的才能进行运算,比如只有数字类型才能进行数学计算。然而JavaScript提供数据类型转换机制,在执行代码的时候,会将数据转换成目标数据类型。

有两种方式,一种是JavaScript自动执行的类型转换,称为隐式类型转换;另一种是开发者手动进行的数据类型转换,就是显式类型转换。

隐式类型转换

JavaScript编译器可以自动转换数据类型,一般情况下是把任意类型的值转换为Number、String、Boolean类型,比如数字和字符串进行相加运算的时候,会把数字类型转化为字符串类型;

1+"43"; // "143"

数字和字符串相减的时候,会把字符串转化为数字,但前提是字符串内容必须是数字:

132-"32"; // 100

数字和Boolean类型相减的时候,会将Boolean转化为数字。false为0,true为1。

在逻辑运算中,数字就会转化为Boolean类型。

隐式类型转换规则如下:

显式类型转换

基本数据类型除了null和undefined之外,其他的都有对应的包装对象,比如:

1、数字类型的Number对象;

2、布尔类型的Boolean对象;

3、字符串类型String对象;

4、Symbol类型的Symbol对象;

5、超大整数类型的BigInt对象。

另外JavaScript还提供了toString函数,将字面量转化为字符串。

· 阅读需 9 分钟

JavaScript和HTML的交互是通过事件来实现的,常见的事件就是鼠标点击事件,click事件、load事件mouseover事件等等,在事件触发时候,会执行绑定在元素上的事件处理程序。

事件流,就是描述从页面中接收到事件的顺序。事件在触发之后,会在目标节点和根节点之间按照一定的顺序在传播,经过的节点都会接收到事件。比如有一个table表格,分别在table表格、tbody、tr、td单元格上绑定click事件。

如果在td上触发点击事件,那么就会产生这样的事件流:

  1. 事件传递顺序是先触发最外层的table元素,然后向里面传递,一次触发tbody、tr和td元素;
  2. 事件传递顺序是从目标元素,也就是我们触发点击的td元素,向外传递,一次触发tr、tbody和table

第一个阶段是事件捕获型事件流,第二阶段是属于事件冒泡型事件流。那一个完整的事件流包含了事件捕获阶段、事件目标阶段和事件冒泡阶段

事件捕获阶段

事件捕获阶段是没有具体的节点接收事件,然后向下一层一层传播,具体的表现如图所示:

在这里插入图片描述

事件目标阶段

这表示事件刚好传播到用户产生行为的元素上,应该是事件捕获阶段的结尾,同时也是事件冒泡的开始

事件冒泡阶段

目标元素接收到事件后,一层一层向外传递。如上图所示。代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<table id="table">
<tbody>
<tr>
<td>td</td>
</tr>
</tbody>
</table>

<script>
const table = document.querySelector("table");
const tbody = document.querySelector("tbody");
const tr = document.querySelector("tr");
const td = document.querySelector("td");


td.addEventListener("click", function () {
console.log("在td元素上触发点击事件")
});

tr.addEventListener("click", function () {
console.log("在tr上触发的事件")
})

tbody.addEventListener("click", function () {
console.log("在tbody上触发事件")
})

table.addEventListener("click",function(){
console.log("在table元素上的事件")
})

</script>
</body>

当点击td元素的时候,结果如下:

在这里插入图片描述

这就事件冒泡现象。

addEventListener函数

使用addEventListener函数绑定事件的话,默认第三个参数是false,也就是默认是按照冒泡事件来处理,上面的代码就是全部默认为冒泡事件类型处理。

如果第三个参数设置为true,那么就是在事件经过该节点的时候,这个事件是按照捕获型事件来处理,比如现在在table元素的事件上做一下修改:

        table.addEventListener("click",function(){
console.log("在table元素上的事件")
},true)

这就是表示只接收捕获型的事件,

在这里插入图片描述

事件处理程序

事件处理程序就是把一个函数赋给一个事件处理属性,有两种方法: 1.通过JavaScript选择器获取元素后,再将函数赋值给对应的事件属性,比如

        const tr = document.querySelector("tr");
td.onclick = function () {
console.log("td元素点击")
}

2.直接在对应元素上设置事件属性,这个事件属性值有两种,一个执行的函数体,另一个是函数名,比如:

    <button onclick="onclickBtn()">点击</button>
<script>
function onclickBtn() {
console.log("td元素点击")
}
</script>

以上两种的事件处理程序属于DOM0级事件处理程序,有点是可以跨浏览器并且简单,缺点就是一个事件处理程序只能绑定一个函数,另外它只支持事件冒泡阶段。

一个处理程序只能绑定一个函数,比如在一个元素上事件属性,然后在JavaScript中给这个元素绑定另一个事件,代码:

    <button onclick="onclickBtn()">点击</button>
<script>
function onclickBtn() {
console.log("td元素点击")
}

const btn = document.querySelector("button");
btn.onclick=()=>{
console.log("js绑定的函数")
}
</script>

结果如图:

在这里插入图片描述

在JavaScript绑定的事件处理程序优先级高于在html元素中定义的事件处理程序。

Event对象

事件在浏览器中是以Event对象的形式存在的,当一个事件被触发的时候,就会产生一个Event对象,这对象包含了所有参与事件的相关信息,包括事件的元素、事件的类型等等,比如:

在这里插入图片描述

根据不同交互场景,获取不同的属性。

阻止事件冒泡

有些场景,我们不需要事件冒泡,比如有一个ul容器,点击li元素,会修改li本身的样式,每一个li都有表示删除的button按钮,如:

    <ul>
<li>
<p>姓名:读心</p>
<p>编号:009</p>
<button class="btn" id="btn">删除</button>
</li>
</ul>
<script>
const li = document.querySelector("li");
const btn = document.querySelector("button");
li.addEventListener("click",function(event){
console.log("点击li操作");
})

btn.addEventListener("click",function(event){
console.log("点击button")
})
</script>

当我们点击按钮的时候,由于冒泡事件的原因,事件冒泡到父元素上,所以两个事件同时触发

在这里插入图片描述

要避免这样的现象,就要在btn上的设置阻止事件冒泡:

        btn.addEventListener("click", function (event) {
event.stopPropagation();
console.log("点击button")
})

事件委托

事件委托就是利用事件冒泡的原理来管理某一个类型的所有事件,利用父元素来代表子元素的某一个类型的事件,比如:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<ul>
<li>1 </li>
<li>2 </li>
<li>3 </li>
<li>4 </li>
<li>5</li>
</ul>
<script>
const li = document.querySelectorAll("li");
for (let index = 0; index < li.length; index++) {
const element = li[index];
element.addEventListener("click", function (event) {
console.log("点击li操作",this.innerText);
})
}
</script>
</body>

</html>

这样的处理方式很消耗性能,如果有100个li元素,那么就是绑定了100个事件,事件处理需要不断的和DOM进行交互,这样导致浏览器重绘和重排的次数也会增加,会延长页面的交互时间。

事件委托就是把事件绑定到父元素上,然后利用事件冒泡原理,当事件进入冒泡阶段时候,通过绑定在父元素上的事件对象来判断当前事件流正在进行的元素:

        const parent = document.querySelector("ul");
parent.addEventListener("click", function (event) {
//获取事件对象
if (event.target.nodeName.toLowerCase() === 'li') {
console.log(event.target.innerText)
}
})

在这里插入图片描述

这就是JavaScript事件!

· 阅读需 3 分钟

Class基本用法

ES6引入的class类,让JavaScript具有更加接近面向对象编程的写法。

ES6之前,想要生成一个实例,只能通过new关键字来调用构造函数来完成。

ES6 使用class来定义类,在类的constructor构造函数中定义实例的属性。原型属性直接在class内部定义并且赋值,类的原型函数的声明,是和constructor构造函数属于同一个层级,并且省略了function关键字。比如:

class Person{
// 原型属性
publicName = "duxin";
constructor(name,age){
//实例属性
this.name = name;
this.age = age;
}
//原型函数
getName(){
return this.name;
}
}

// 通过class创建实例
const child = new Person("duxinyues",18);
console.log(child.getName()); // duxinyues

在ES5中的写法为:

//构造函数
function Person(name,age){
//实例属性
this.name = name;
this.age = age;
}

Person.prototype.publicName = "duxin";
Person.prototype.getName = function(){
return this.name
}

class中有构造函数、静态属性和函数:

constructor构造函数

constructor构造函数,是class类中必须有的一个函数,如果没有手动添加,class也会自动加上一个空的构造函数。

构造函数默认返回对象的实例,也就是默认了this的指向。也可以手动修改构造函数的返回值。

静态属性和函数

如果使用static关键字来修饰静态属性的话,那么实例就不能访问该属性,这个属性只能通过类自身来访问。比如:

class Foo {
static name = "duxin";
static getName() {
return this.name
}
}

console.log(Foo.getName()); //duxin

静态函数中的this指向是类本身

Class继承

ES6新增的extends关键字,可以快速实现类的继承。

在子类的constructor构造函数中,使用super函数来执行父类的构造函数,然后再来执行子类。比如:

class Parson {
constructor() {
this.name = "duxin"
}

}


class Child extends Parson {
constructor() {
super();
this.age = "908"
}
}

const child = new Child()
console.log(child.name); // duxin

这就是ES6中class的使用。

· 阅读需 6 分钟

01、null和undefined

undefined是全局对象的一个属性,当一个变量没有赋值或者访问一个对象不存在的属性,这时候都是undefined。

null:表示是一个空对象。在需要释放一个对象的时候,直接赋值为null即可。

02、箭头函数

箭头函数是ES6新增的,简化函数的定义。箭头函数没有自身的this,所以this是从外部获取的,也就是继承了外部的执行上下文。

箭头函数不能作为构造函数,如果通过call()或者apply()调用箭头函数的时候,不需要绑定this,直接传入参数即可。

03、call、apply和bind的作用

bind、call和apply都是改变函数this的指向。 bind在改变this的时候,返回的是一个改变执行上下文的函数,不会立即执行。 call和apply在改变this指向同时也执行该函数。 bind只有一个参数,call和apply可接收多个参数,第一个参数是this的指向。apply的第二参数是一个数组。

04、函数的this

this是函数的执行上下文,分为全局执行上下文和函数执行上下文。

this在严格模式下,指向的是undefined,非严格模式下默认指向window。

函数的this,在函数被调用的时候,指向的是函数的调用者,也就是谁调用,就指向谁。

如果通过new 构造函数创建一个新的对象,那么构造函数的中的this指向新对象本身。

普通函数不继承this,箭头函数没有this,它是继承外部的this。

05、变量提升

变量提升是指js的变量和函数在编译的时候提升到最前面。

造成变量提升的现象,是因为使用var关键字声明的变量,变量提升的时候,只有声明在提升,变量赋值并没有提升,在变量初始化之前访问该变量,就会返回undefined。使用let或者const声明变量,就形成暂时性死区,在let或者const声明变量之前访问变量会报错。

06、map和forEach的区别

map有返回值,可以开辟新的空间,return返回一个长度和原数组长度一样的新数组。

forEach函数没有返回值,返回的undefined。

map的处理速度比forEach快,返回新数组,这样方便链式调用其他数组方法,比如filter、reduce等等。

07、怎么理解事件循环、微任务和宏任务

浏览器的事件循环是执行js代码的时候,遇见同步任务,直接推进调用栈中执行,遇到异步任务时候,将异步任务挂起,等到异步任务有返回之后再推到任务队列中。

当调用栈中所有的同步任务执行完成,将任务队列中的任务按照顺序执行。重复执行这一系列的行为就是事件循环。

异步任务又分为宏任务和微任务。宏任务就是任务队列中的任务,每一个宏任务中包含一个微任务队列;

微任务:就是等宏任务中的主要功能执行完成后,渲染引擎并没有立即执行下一个宏任务,而是执行当前宏任务中的微任务。

宏任务包含:script标签内的代码、定时器、Ajax请求

微任务:Promise

08、跨站点请求伪造CSRF

攻击者盗用用户的身份,以用户的身份发起恶意请求。但是对于服务器来说,这个请求是合理的。

预防CSR攻击方法:

  1. 使用验证码,强烈要求用户和应用进行交互
  2. 在http中referer字段,检查是否是从正确的域名访问过来,它记录了http请求的来源地址
  3. 使用token验证,在http请求头中添加token字段,在服务器端设置一个拦截器来验证token,如果token无效,那么拒绝访问

09、XSS攻击

XSS攻击是脚本攻击,攻击者通过向web页面插入script代码,在用户浏览这个页面时候,执行script的脚本代码,达到攻击的目的。

预防:对数据进行严格的输出编码,比如URL编码、css编码、JavaScript编码。

10、浏览器如何渲染页面的

浏览器从服务器获取到html后,将html转化为DOM树,再将css样式转化为对应的stylesheet,根据DOM树和stylesheet绘制成页面。

· 阅读需 11 分钟

01、前端存储:cookie、sessionStorage和localStorage的区别

相同点:

  1. cookie、sessionStorage和localStorage都属于浏览器本地存储;
  2. cookie、sessionStorage和localStorage存储遵循的同源策略。sessionStorage还限制是同一个页面。

区别是:

  1. cookie是由服务器端写入的,sessionStorage和localStorage是前端写入的;
  2. cookie的有效期,是在服务端设置好的。sessionStorage则是在页面关闭后自动清除,localStorage则可以长期保存,除非手动清除。
  3. cookie的存储空间比较小,每一个cookie大概是4kb。sessionStorage和localStorage存储空间比较大,大约为5M。
  4. 前端向后端发起请求的时候,会自动携带cookie。localStorage和sessionStorage则不会。

cookie是一般用于存储验证信息SessionID或者token;localStorage一般是用来存储一些不易变动的数据,这样减小服务器的压力。sessionStorage是用来检测用户是否是刷新进入页面的,比如音乐播放器进度条。

02、JavaScript数据类型

JavaScript数据类型分为两种:一种基本数据类型,一种是引用数据类型

基本数据类型:number、string、boolean、null、undefined、symbol和BigInt。 引用数据类型:object,包括了function、array、正则、date日期和Math数学函数。

基本数据类型和引用数据类型,它们在内存中的存储方式不同。基本数据类型是直接存储在栈内存中。引用数据类型是存储在堆内存中的,在栈内存中存储了指针,这个指针指向在堆内存中的实体。

基本数据类型中symbol是ES6新增的一种数据类型,它的特点就是不能有重复数据,可以用来作为对象的key。symbol创建的数据是唯一的,所以 Symbol() !== Symbol()。

BigInt数据类型,也是ES6新增,作用就是扩大了数据的范围,能够解决普通数据类型范围报错的问题。BigInt有两种使用方法:一种是直接在整数后面加n。另一种方式是调用BigInt构造函数。

03、对闭包的理解

函数和词法环境绑定在一起,这样的组合就是闭包,比如有一个函数A,它return一个函数B。函数B是可以访问到函数A内部定义的变量。函数A执行结束后,函数A中声明的变量并不会被销毁。

闭包的优点:让函数作用域中的变量不会因为函数执行结束而被销毁。也可以在外部访问到函数内部的局部变量; 闭包的缺点:垃圾回收器不会销毁闭包中的变量,这样就造成内存泄漏。

04、说一下Promise

Promise是实现异步的一种方式,解决了异步多层嵌套回调的问题,提升了代码的可读性,同时让我们所写的代码更利于维护。

Promise有三个状态:pendding、resolve和reject。在Promise的整个过程中,只发生一次状态转变。由pendding转为resolve或者reject。

Promise构造函数接收一个函数作为参数,这个函数有两个参数:resolve和reject,resolve和reject都是函数。

resolve函数的作用是把Promise由等待状态改为成功状态;reject是把Promise由等待状态转变为失败状态。

在Promise构造函数创建实例完成后,通过then函数来接收成功的回调函数,通过catch函数接受失败的回调函数,比如:

        const func = function (parma) {
return new Promise((resolve, reject) => {
if (parma > 2) {
resolve(parma)
}
reject(parma)
})
}

func(0).then(res => {
console.log("结果", res)
}).catch(err => console.log("报错", err))

Promise的特点:

  1. Promise的状态,不收到外界的影响,当Promise发生状态变化后,Promise的生命周期也就结束了。
  2. Promise只发生一次状态改变。
  3. resolve的参数是then方法中回调函数的参数;reject的参数是catch方法中回调函数的参数。

Promise的其他方法:

Promise.all()

把多个Promise包装成一个Promise对象,等参数里面所有的Promise都返回成功了,才触发成功,否则返回失败结果,比如:

        const func = new Promise((resolve, reject) => {
resolve(1)
})
const func2 = new Promise((resolve, reject) => {
reject(1)
})
const allPromise = Promise.all([func, func2])
allPromise.then(res => {
console.log("结果", res)
}).catch(err => {
console.log("报错", err)
})

这段代码的结果是失败状态:1

如果func2调用的resolve(1)的话,那么这段代码的结果为成功状态:[1,1]

Promise.any()

接收一个Promise对象集,只要有一个Promise返回成功,那么就返回这个Promise成功的值,比如:

        const func = new Promise((resolve, reject) => {
resolve(1)
})
const func2 = new Promise((resolve, reject) => {
reject(0)
})
const promise = Promise.any([func, func2])
promise.then(res => {
console.log("结果", res)
}).catch(err => {
console.log("报错", err)
})

这段代码的结果为:结果 1

Promise.race()

只要Promise集合里面,有一个子Promise返回成功或者失败,那么父Promise将子Promise的状态返回,然后结束Promise的生命周期。比如:

        const func = new Promise((resolve, reject) => {
reject(1)
})
const func2 = new Promise((resolve, reject) => {
resolve(0)
})
const promise = Promise.race([func, func2])
promise.then(res => {
console.log("结果", res)
}).catch(err => {
console.log("报错", err)
})

05、什么是跨域,怎么解决跨域

跨域:就是当前页的请求地址和当前页面的地址中,协议、域名、端口,其中一个不同,就造成了跨域。原因是浏览器为了保护网页安全做出的同原协议策略。

跨域的解决方式:

  1. cors;通过设置后端允许跨域访问
  2. node中间件、Nginx反向代理:跨域是浏览器限制不能跨域访问服务器,而node中间件和Nginx反向代理,是向代理服务器发起请求,代理服务器再向后端服务器发起请求。服务器和服务器之间不存在同源限制。

出现跨域的场景,一般是前后端分离开发、调用第三方接口。

06、什么是BFC

BFC是块级格式化上下文,是web页面中一个独立的渲染区域,内部元素的渲染不会影响到区域外面的其他元素。

BFC布局规则是: 内部元素会在垂直方向堆叠摆放,元素上下之间的距离,是由margin来决定的,而相邻的两元素的margin会发生重叠。

07、js判断数据类型的方式

JavaScript有3种方法判断数据类型:typeof、instanceof、Object.prototype.toString.call()。

typeof:用来判断基本数据类型的,如果使用typeof来判断引用数据类型的话,除了function会返回“function”,其他的都会返回“object”。

instanceof:用来区分引用数据类型、判断实例是否属于某一个构造函数。检测过程比较繁琐,而且对于undefined、null和symbol数据类型是无法检测的

Object.prototype.toString.call():可以用来检测所有的数据类型,返回的是该数据类型的字符串。

instanceof的原理是验证当前对象的原型prototype是不是出现在实例的原型链proto上,如果在,就返回true,否则返回false。

Object.prototype.toString.call()的原理是Object.prototype.toString表示一个返回对象数据类型的字符串,call()方法是改变this的指向,也就是把Object.prototype.toString()方法指向不同的数据类型上。

08、CSS样式优先级

在CSS样式中!important的优先级是最高的。其次是内联样式。

CSS选择器的优先级:id选择器 > (类选择器 | 伪类选择器|属性选择器) > (后代选择器 | 伪元素) > 子选择器或者相邻选择器 > 通配选择器。

09、JavaScript异步的方式

回调函数:是异步操作的基本方式,也是最简单,容易理解和实现的,比如常见的AJAX。但是回调函数不利于代码维护和阅读,代码结构混乱,流程难以追踪。

Promise、async/await

这三种是最常见的,实现异步操作的方式。

10、数组去重的方式

  1. 定义一个新数组,然后遍历数组过程中,每次判断新数组中是否存在元素,不存在的话,就将元素push进去。
  2. 利用Set数据类型的不重复特点,new一个Set,参数就是需要去重的数组,set会自动删除重复数据,然后将set转化为数组返回。
  3. reduce + includes:利用reduce遍历数组和传入一个空数组作为去重后的新数组,在内部再来判断新数组中是否存在当前元素,如果不存在,那么就将元素push进去。