web前端面试题

一. JavaScript常见字符串方法、数组方法、对象方法

字符串方法:

  1. charAt( ) 方法从一个字符串中返回某个下标上的字符 concat( ) 方法将一个或多个字符串与原字符串连接合并,形成一个新的字符串并返回。

  1. search( ) 获取某个字符或者字符串片段首次出现的位置 match( ) 检索字符串或者正则表达式 replace( ) 替换 replace(参数 1,参数 2); 参数 1:替换谁 参数 2:替换值 split( 参数 1,参数 2 ) 字符串切割,切割后返回数组

  1. slice( ) 和 substring( ) ( 开始位置,结束位置 ) 获取两个索引值之间的字符串片段 从下标 2 开始截取,到下标 4 结束,但不包含 4 substr ( 开始位置,截取的长度 ) 获取两个索引值之间的字符串片段

  1. indexOf( ) 获取某个字符或者字符串首次出现的位置,找到则返回索引值,找不到则返回-1 lastIndexOf( ) 获取某个字符或者字符串最后出现的位置,找到则返回索引值,找不到则返回-1

数组方法

  1. push() 可以添加一个或多个参数到数组的尾部,添加之后原来的数组会发生改变,返回的是添加后的数组的长度

  1. pop() 从数组尾部删除一个元素,原数组会发生改变,返回数组中被删除的元素

  1. unshift() 可以添加一个或多个参数到数组的头部,添加后原来的数组会发生改变,返回的是添加后的数组的长度

  1. shift() 从数组头部删除一个元素,原数组会发生改变,返回数组中被删除的元素

  1. slice() 从数组中截取,如果不传参,会返回原数组。返回截取的值,两个参数 一个是开始的位置,第二个是结束的位置,原数组不变,最多两个参数

  1. splice()截取类 没有参数,返回空数组,原数组不变;一个参数,从该参数表示的索引位开始截取,直至数组结束,返回截取的 数组,原数组改变;两个参数,第一个参数表示开始截取的索引位,第二个参数表示截取的长度,返回截取的 数组,原数组改变;三个或者更多参数,第三个及以后的参数表示要从截取位插入的值。

  1. reverse() 数组翻转

  1. sort() 数组排序

  1. join() 数组拼接

  1. isArray() 判断是否是数组

  1. toString() 数组转字符串

  1. concat( ) 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组

  1. 遍历数组的方法 它们的参数都是回调函数forEach 循环 没有 return 对原数组发生改变

  1. filter 返回所有满足条件的新数组 返回值就是新数组

  1. map 遍历数组,可重新构建一个新的数组 返回值就是新数组

  1. find 查找数组中满足条件的第一个数组项 返回这个数组项

  1. findIndex 查找数组中满足条件的第一个数组项的索引 返回这个数组项的索引或者-1

  1. some 遍历数组中,数组中至少有一个满足条件的数组项,返回 true 否则 false

  1. every 遍历数组 所有的数组项都满足条件时 返回 ture 否则 false

对象的方法

1.Object.assign()作用:将所有可枚举的自身属性从一个或多个源对象复制到目标对象。语法:Object.assign(target,…sources)参数:target:目标对象——应用源属性的对象,修改后返回。sources:源对象——包含你要应用的属性的对象。返回值:修改后的目标对象。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }复制代码

注意:如果源对象中的属性具有相同的键,则目标对象中的属性会被源中的属性覆盖。较晚来源的属性会覆盖较早来源的属性。2.Object.values()作用:返回给定对象自己的可枚举属性值的数组,其顺序与 for…in 循环提供的顺序相同。语法:Object.values(obj)参数:obj:要返回其可枚举自身属性值的对象。返回值:包含给定对象自己的可枚举属性值的数组。

const object1 = {
  a: 'somestring',
  b: 42,
  c: false
};
console.log(Object.values(object1));
// expected output: Array ["somestring", 42, false]复制代码

3.Object.prototype.hasOwnProperty()作用:返回一个布尔值,指示对象是否具有指定的属性作为它自己的属性。如果指定的属性是对象的直接属性,则该方法返回 true — 即使值为 null 或未定义。如果该属性是继承的或根本没有声明,则返回 false。语法:hasOwnProperty(prop)参数:prop:要测试的属性的字符串名称或符号。返回值:如果对象将指定的属性作为自己的属性,则返回true;否则为false。

const object1 = {};
object1.property1 = 42;
console.log(object1.hasOwnProperty('property1'));// trueconsole.log(object1.hasOwnProperty('toString'));// falseconsole.log(object1.hasOwnProperty('hasOwnProperty'));// false复制代码

4.Object.keys()作用:返回给定对象自己的可枚举属性名称的数组,以与普通循环相同的顺序迭代。语法:Object.keys(obj)参数:obj:要返回可枚举自身属性的对象。返回值:表示给定对象的所有可枚举属性的字符串数组

const object1 = {
  a: 'somestring',
  b: 42,
  c: false
};
console.log(Object.keys(object1));
// expected output: Array ["a", "b", "c"]复制代码

5.Object.prototype.toString()作用:返回一个表示对象的字符串。当对象将被表示为文本值或以期望字符串的方式引用对象时,将自动调用此方法 id。默认情况下,toString() 方法由从 Object 继承的每个对象继承。语法:toString()返回值:表示对象的字符串。

functionDog(name) {
  this.name = name;
}
const dog1 = newDog('Gabby');
Dog.prototype.toString = functiondogToString() {
  return`${this.name}`;
};
console.log(dog1.toString());
// expected output: "Gabby"复制代码

注意:对于 Numbers 和 Big Ints,toString() 采用可选参数 radix,其值必须最小为 2,最大为 36。6. Object.freeze()作用:冻结一个对象,这意味着它不能再被更改。冻结对象可防止向其添加新属性,防止删除现有属性,防止更改现有属性的可枚举性、可配置性或可写性,并防止更改现有属性的值。它还可以防止其原型被更改。语法:Object.freeze(obj)参数:obj:要冻结的对象。返回值:传递给函数的对象。

const obj = {
 prop: 42
};
Object.freeze(obj);
obj.prop = 33;
// Throws an error in strict modeconsole.log(obj.prop);
// expected output: 42复制代码

7.Object.entries()作用:返回给定对象自己的可枚举字符串键属性 [key, value] 对的数组。它类似于使用 for…in 循环进行迭代,除了 for…in 循环还会枚举原型链中的属性。属性的顺序与通过手动循环对象的属性值给出的顺序相同。语法:Object.entries(obj)参数:obj:要返回其自己的可枚举字符串键属性 [key, value] 对的对象。返回值:给定对象自己的可枚举字符串键属性 [key, value] 对的数组。

const object1 = {name: "David", age: 23};
for (const [key, value] ofObject.entries(object1)) {
  console.log(`${key}: ${value}`);
}
// "name: David"// "age: 23"复制代码

8.Object.is()作用:判断两个值是否相同的方法。语法:Object.is(value1, value2);参数:value1:要比较的第一个值。value2:要比较的第二个值。返回值:一个布尔表达式,指示两个参数是否具有相同的值。

// Case 1: Evaluation result is the same as using '==='Object.is(25, 25);                // trueObject.is('foo', 'bar');          // falseObject.is(foo, foo);              // true// Case 2: Signed zeroObject.is(0, -0);                 // falseObject.is(0n, -0n);               // true// Case 3: NaNObject.is(NaN, 0/0);              // trueObject.is(NaN, Number.NaN)        // true复制代码

二 js检测数据类型的方法

数据类型判断大概有四种 typeof、instanceof、constructor、Object.prototype.toString.call() 比如说检测 num=10 的数据类型

  1. Type: typeof 检测数据类型会返回对应的数据类型小写字符。 引用数据类型中的:Array,Object,Date,RegExp。不可以用 typeof 检测。都会返回小写的 object console.log(typeof num);

  1. instanceof 除了使用 typeof 来判断,还可以使用 instanceof。instanceof 运算符需要指定一个构造函数,或者说指定一个特定的类型,它用来判断这个构造函数的原型是否在给定对象的原型链上。 console.log(arr instanceof Array);

  1. constructor constructor 是 prototype 对象上的属性,指向构造函数。我们可以用实例的隐式原型去找到构造函数的值。 console.log(num.proto.constructor);

  1. 使用 Object.prototype.toString.call()检测对象类型 可以通过 toString() 来获取每个对象的类型。每个对象都能通过 Object.prototype.toString() 来检测 console.log(Object.prototype.toString.call(num));

三 ES6新的数据类型和应用场景

一、BigIntBigInt是ES10新加的一种JavaScript数据类型,用来表示表示大于 2^53 - 1 的整数,2^53 - 1是ES10之前,JavaScript所能表示最大的数字

const bigNum = BigInt(1728371927189372189739217)
console.log(typeof bigNum) // bigint复制代码

二、SymbolSymbol是ES6中新增的一种基本数据类型,它是一个函数,会返回一个Symbol类型的值,每一个Symbol函数返回的值都是唯一的,它们可以被作为对象属性的标识符。

typeof Symbol(); // Symbol基本用法:使用Symbol值作为对象的key

let name=Symbol() //声明一个变量,数据类型为symbollet obj={
    [name]:"hsq"
   }
console.log(obj.name);// undefinedconsole.log(obj[name]);// hsq复制代码

注意:当我们使用变量去定义一个对象key时需要使用[ ]包裹着,否则就会被自动转化成string类型,因为属性使用变量进行定义,所以获取属性值需要使用obj[key]这种方式获取

但是Symbol作为属性的属性不会被枚举出来

let name=Symbol() //声明一个变量,数据类型为symbollet obj={
    [name]:"hsq",
    age:22
   }
   console.log(Object.keys(obj)) // [ 'age' ]for(const key in obj) {
  console.log(key) // age
  }
   console.log(JSON.stringify(obj)) // {"age":22}
获取Symbol属性

  let name=Symbol('name') //声明一个变量,数据类型为symbollet obj={
    [name]:"hsq",
    age:22
   }
复制代码
// 方法一console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol(name) ]// 方法二console.log(Reflect.ownKeys(obj)) // [ 'name', 'age', Symbol(name) ]
应用场景:使用Symbol定义类的私有属性

   classLogin {
  constructor(username, password) {
    constPASSWORD = Symbol()
    this.username = username
    this[PASSWORD] = password
  }
  checkPassword(pwd) { returnthis[PASSWORD] === pwd }
}
 
const login = newLogin('123456', 'hahah')
 
console.log(login.PASSWORD) // undefinedconsole.log(login[PASSWORD]) // 报错复制代码

三、set(集合)Set 对象(集合)类似于数组,且成员的值都是唯一的(通俗的理解:不能出现相同的元素)

常用语法

// 可不传数组const set1 = newSet()
set1.add(1)
set1.add(2)
console.log(set1) // Set(2) { 1, 2 }// 也可传数组const set2 = newSet([1, 2, 3])
// 增加元素 使用 add
set2.add(4)
set2.add('林三心')
console.log(set2) // Set(5) { 1, 2, 3, 4, '林三心' }// 是否含有某个元素 使用 hasconsole.log(set2.has(2)) // true// 查看长度 使用 sizeconsole.log(set2.size) // 5// 删除元素 使用 delete
set2.delete(2)
console.log(set2) // Set(4) { 1, 3, 4, '林三心' }复制代码

不重复性

// 两个对象都是不同的指针,所以没法去重const set1 = newSet([1, {name: '林三心'}, 2, {name: '林三心'}])
console.log(set1) // Set(4) { 1, { name: '林三心' }, 2, { name: '林三心' } }复制代码

// 如果是两个对象是同一指针,则能去重

const obj = {name: '林三心'}
const set2 = newSet([1, obj, 2, obj])
console.log(set2) // Set(3) { 1, { name: '林三心' }, 2 }复制代码

咱们都知道 NaN !== NaN,NaN是自身不等于自身的,但是在Set中他还是会被去重

const set = newSet([1, NaN, 1, NaN])
console.log(set) // Set(2) { 1, NaN }复制代码

最常用来去重使用,去重方法有很多但是都没有它运行的快。

const arr = [1, 2, 3, 4, 4, 5, 5, 66, 9, 1]
 
// Set可利用扩展运算符转为数组哦const newArr = [...newSet(arr)]
console.log(newArr) // [1,  2, 3, 4, 5, 66, 9]复制代码

四、mapMap 是一组键值对的结构,和 js对象类似。

基本用法

//初始化Map需要一个二维数组(请看 Map 数据结构),或者直接初始化一个空Map

let map = newMap();
 
//添加key和value值
map.set('Amy','女')
map.set('liuQi','男')
 
//是否存在key,存在返回true,反之为false
map.has('Amy') //true
map.has('amy') //false//根据key获取value
map.get('Amy') //女//删除 key为Amy的value
map.delete('Amy')
map.get('Amy') //undefined  删除成功复制代码

Map对比object最大的好处就是,key不受类型限制

// 定义mapconst map1 = newMap()
// 新增键值对 使用 set(key, value)
map1.set(true, 1)
map1.set(1, 2)
map1.set('哈哈', '嘻嘻嘻')
console.log(map1) // Map(3) { true => 1, 1 => 2, '哈哈' => '嘻嘻嘻' }// 判断map是否含有某个key 使用 has(key)console.log(map1.has('哈哈')) // true// 获取map中某个key对应的value 使用 get(key)console.log(map1.get(true)) // 1// 删除map中某个键值对 使用 delete(key)
map1.delete('哈哈')
console.log(map1) // Map(2) { true => 1, 1 => 2 }// 定义map,也可传入键值对数组集合const map2 = newMap([[true, 1], [1, 2], ['哈哈', '嘻嘻嘻']])
console.log(map2) // Map(3) { true => 1, 1 => 2, '哈哈' => '嘻嘻嘻' }复制代码

————————————————

四 js原型链

深拷贝、浅拷贝和应用场景

简版

浅拷贝:拷贝基本数据类型为他的值,拷贝引用数据类型为地址,生成新的数据,修改新的数据会影响原数据,实际开发常用的方法有:object.assgin,扩展运算符等等

深拷贝:在内存中开辟一个新的栈空间保存新的数据,修改新数据不会影响到原数据,开发中常用的方法有:loadsh 中的_.cloneDeep()方法,JSON.stringify()

(@刘进强)

#一、数据类型存储

前面文章我们讲到,JavaScript中存在两大数据类型:

  • 基本类型

  • 引用类型

基本类型数据保存在在栈内存中

引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中

#2、浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

下面简单实现一个浅拷贝

function shallowClone(obj) {
  const newObj = {}
  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      newObj[prop] = obj[prop]
    }
  }
  return newObj
}
复制代码

在JavaScript中,存在浅拷贝的现象有:

  • Object.assign

  • Array.prototype.slice(), Array.prototype.concat()

  • 使用拓展运算符实现的复制

#Object.assign

var obj = {
  age: 18,
  nature: ['smart', 'good'],
  names: {
    name1: 'fx',
    name2: 'xka',
  },
  love: function () {
    console.log('fx is a great girl')
  },
}
var newObj = Object.assign({}, fxObj)
复制代码

#slice()

const fxArr = ['One', 'Two', 'Three']
const fxArrs = fxArr.slice(0)
fxArrs[1] = 'love'
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]复制代码

#concat()

const fxArr = ['One', 'Two', 'Three']
const fxArrs = fxArr.concat()
fxArrs[1] = 'love'
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]复制代码

#拓展运算符

const fxArr = ['One', 'Two', 'Three']
const fxArrs = [...fxArr]
fxArrs[1] = 'love'
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]复制代码

#3、深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

常见的深拷贝方式有:

  • _.cloneDeep()

  • jQuery.extend()

  • JSON.stringify()

  • 手写循环递归

#_.cloneDeep()

const _ = require('lodash')
const obj1 = {
  a: 1,
  b: { f: { g: 1 } },
  c: [1, 2, 3],
}
const obj2 = _.cloneDeep(obj1)
console.log(obj1.b.f === obj2.b.f) // false复制代码

#jQuery.extend()

const $ = require('jquery')
const obj1 = {
  a: 1,
  b: { f: { g: 1 } },
  c: [1, 2, 3],
}
const obj2 = $.extend(true, {}, obj1)
console.log(obj1.b.f === obj2.b.f) // false复制代码

#JSON.stringify()

const obj2 = JSON.parse(JSON.stringify(obj1))
复制代码

1

但是这种方式存在弊端,会忽略undefined、symbol和函数

const obj = {
  name: 'A',
  name1: undefined,
  name3: function () {},
  name4: Symbol('A'),
}
const obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2) // {name: "A"}复制代码

#循环递归

functiondeepClone(obj, hash = newWeakMap()) {
  if (obj === null) return obj // 如果是null或者undefined我就不进行拷贝操作if (obj instanceofDate) returnnewDate(obj)
  if (obj instanceofRegExp) returnnewRegExp(obj)
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝if (typeof obj !== 'object') return obj
  // 是对象的话就要进行深拷贝if (hash.get(obj)) return hash.get(obj)
  let cloneObj = new obj.constructor()
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj)
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash)
    }
  }
  return cloneObj
}
复制代码

#4、区别

下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别

从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样

浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

// 浅拷贝const obj1 = {
  name: 'init',
  arr: [1, [2, 3], 4],
}
const obj3 = shallowClone(obj1) // 一个浅拷贝方法
obj3.name = 'update'
obj3.arr[1] = [5, 6, 7] // 新旧对象还是共享同一块内存

console.log('obj1', obj1) // obj1 { name: 'init',  arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3', obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }复制代码

但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

// 深拷贝const obj1 = {
  name: 'init',
  arr: [1, [2, 3], 4],
}
const obj4 = deepClone(obj1) // 一个深拷贝方法
obj4.name = 'update'
obj4.arr[1] = [5, 6, 7] // 新对象跟原对象不共享内存

console.log('obj1', obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4', obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }复制代码

#小结

前提为拷贝类型为引用类型的情况下:

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址

  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

五 宏任务和微任务

宏任务与微任务的理解宏任务与微任务是什么?宏任务与微任务表示异步任务的两种分类宏任务:包括整体代码script(可以理解为外层同步代码)、settimeout、setInterval、i/o、ui render微任务:promise、object.observe、MutationObserver(监听DOM树的变化)因为异步任务放在队列中,自然而然宏任务与微任务就存放在宏任务队列与微任务队列中代码的执行顺序:先执行主线程执行栈中的代码(同步任务),那异步任务怎样执行的?先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕(事件循环EventLoop)。简言之就是放入队列时宏任务、微任务不分优先级,但是将队列中的代码拿到执行栈中执行微任务优先(当微任务队列的所有任务全部执行完后,才开始执行宏任务)

相关试题

setTimeout(function() {
    console.log('1');
})
newPromise(function(resolve) {
    console.log('2');
}).then(function() {
    console.log('3');
})
console.log('4');
//打印顺序 2 4 3 1复制代码

执行顺序

遇到setTimeout,异步宏任务将其放到宏任务列表中 命名为time1new Promise 在实例化过程中所执行的代码都是同步执行的(function中的代码),输出2将Promise中注册的回调函数放到微任务队列中,命名为then1执行同步任务cosole…log(‘4’) ,输出4,至此执行栈中的代码执行完毕从微任务队列取出任务then1到主线程中,输出3,至此微任务队列为空从宏任务队列中取出任务time1到主线程中,输出1,至此宏任务队列为空

console.log(1)
setTimeout(function(){
  console.log(2);
  let promise = newPromise(function(resolve, reject) {
      console.log(7);
      resolve()
  }).then(function(){
    console.log(8)
  });
},1000);
setTimeout(function(){
  console.log(10);
  let promise = newPromise(function(resolve, reject) {
      console.log(11);
      resolve()
  }).then(function(){
    console.log(12)
  });
},0);
let promise = newPromise(function(resolve, reject) {
    console.log(3);
    resolve()
}).then(function(){
  console.log(4)
}).then(function(){
  console.log(9)
});
console.log(5)

复制代码

执行顺序

执行同步任务console.log(1) ,输出1遇到setTimeout放到宏任务队列中,命名time1遇到setTimeout放到宏任务队列中,命名time2new Promise 在实例化过程中所执行的代码都是同步执行的(function中的代码),输出3将Promise中注册的回调函数放到微任务队列中,命名为then1将Promise中注册的回调函数放到微任务队列中,命名为then2执行同步任务console.log(5),输出5从微任务队列取出任务then1到主线程中,输出4从微任务队列取出任务then2到主线程中,输出9,至此微任务队列为空从宏任务队列中取出time2(注意这里不是time1的原因是time2的执行时间为0)执行同步任务console.log(10),输出10new Promise 在实例化过程中所执行的代码都是同步执行的(function中的代码),输出11将Promise中注册的回调函数放到微任务队列中,命名为then3,至此宏任务time2执行完成从微任务队列取出任务then3到主线程中,输出12,至此微任务队列为空从宏任务队列中取出time1,至此宏任务队列为空执行同步任务console.log(2),输出2new Promise 在实例化过程中所执行的代码都是同步执行的(function中的代码),输出7将Promise中注册的回调函数放到微任务队列中,命名为then4,至此宏任务time1执行完成从微任务队列取出任务then3到主线程中,输出8,至此微任务队列为空练手题

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    newPromise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
newPromise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    newPromise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
// 1 7 6 8 2 4 3 5 9 11 10 12复制代码

再附一张同步任务和异步任务的执行过程

六 什么是递归?应用场景

1.递归的概念

简单的说:递归就是方法自己调用自己 ,每次调用时传入不同的变量 .递归有助于编程者解决复杂的问题 ,同时可以让代码变得简洁。(个人觉得递归很类似于循环)

2.递归能解决什么样的问题

1)各种数学问题如: 8皇后问题 ,汉诺塔,阶乘问题,迷宫问题,球和篮子的问题 。

2)各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.

3)将用栈解决的问题–>递归代码比较简洁

3.递归需要遵守的重要规则

1)执行一个方法时,就创建一个新的受保护的独立空间(栈空间) 2)方法的局部变量是独立的,不会相互影响,比如 n变量 3)如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据. 4)递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError,死龟了:) 5)当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕

举例说明:

1) 递归处理数组扁平化

let arr =[100,200,300,[400,500,[49,59,80,[232,5435,77]]]]

const add=(arr)=>{

// console.log(arr);

let num =0;

arr.forEach(item => {

    if(Array.isArray(item)){
    
        num+=add(item)
        
    }else{
    
        num+=item
    }
    
});

    return num 
}

console.log(add(arr));
复制代码
复制代码

2)斐波拉契数列

// 1 1 2 3 5 8 13 21 34

function fn(n){

if(n==1 || n==2) return 1;

return fn(n-1)+fn(n-2)

}   

fn(5)

console.log(fn(6));
复制代码
复制代码

3)数组对象复杂求和

letarr1= [
        {
            name:"张三",
            money:100,
            children: [
                { name:"张欢欢", money:200 },
                {
                    name:"张乐乐",
                    money:100,
                    children: [
                        { name:"张小欢", money:300 },
                        { name:"张小乐", money:400 },
                    ],
                },
            ],
        },
        {
            name:"李四",
            money:100,
            children: [
                { name:"李红红", money:500 },
                { name:"李明明", money:600 },
            ],
        },
    ];letnum=0;functionfn(obj) {

        obj.forEach((item, index)=> {
            if(item.children) {
                fn(item.children);
            } else {
                returnnum+=item.money
            }
        })

    }
    fn(arr1)console.log(num);复制代码复制代码

4) 递归组件

-   组件在它的模板内可以递归的调用自己,只要给组件设置 name 组件就可以了。
-   不过需要注意的是,必须给一个条件来限制数量,否则会抛出错误: `max stack size exceeded`-   组件递归用来开发一些具体有未知层级关系的独立组件。比如:联级选择器和树形控件
复制代码
复制代码
function clone(o) {
        var temp = {}
        for (var keyin o) {
            if (typeof o[key] == 'object') {
                temp[key] = clone(o[key])
            } else {
                temp[key] = o[key]
            }
        }
        return temp
    }

复制代码
复制代码

递归的问题

当然, 这个世界上没有啥时万能的, 递归也不例外, 首先递归并不一定适用所有情况, 很多情况用迭代远远比用递归好了解, 其次, 相对来说, 递归的效率往往要低于迭代的实现, 同时, 内存耗用也会更大, 虽然这个时候可以用尾递归来优化, 但是尾递归并不是一定能简单做到.它是依据数据结构的栈的原理,不断开辟新的内存空间以满足程序需要,而不是不断改变已有内存空间的值来满足程序需要,所以递归是一种极具消耗内存资源的算法思维,所以在现实项目中,除非代码量影响过大,否则能不用递归就不用递归.

七 防抖和节流?应用场景

简版

防抖和节流是性能优化手段

什么是防抖? 防抖:单位时间内,频繁触发事件,只执行最后一次。 防抖的主要应用场景:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求

  • 手机号、邮箱验证输入检测

什么是节流? 节流:单位时间内,频繁触发事件,只执行一次。 节流的主要应用场景:

  • 高频事件 例如 resize 事件、scroll 事件

  • 手机号、邮箱验证输入检测

相同点:

  • 都可以通过使用 setTimeout 来实现

  • 降低回调执行频率。节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,处理回调,利用 clearTimeout 和 setTimeout 来实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能

  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

#一、是什么

本质上是优化高频率执行代码的一种手段

如:浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用throttle(抖流)和debounce(节防)的方式来减少调用频率

#定义
  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效

  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应

假设电梯有两种运行策略 debounce 和 throttle,超时设定为 15 秒,不考虑容量限制

电梯第一个人进来后,15 秒后准时运送一次,这是节流

电梯第一个人进来后,等待 15 秒。如果过程中又有人进来,15 秒等待重新计时,直到 15 秒后开始运送,这是防抖

#代码实现

#节流

完成节流可以使用时间戳与定时器的写法

使用时间戳写法,事件会立即执行,停止触发后没有办法再次执行

functionthrottled1(fn, delay = 500) {
  let oldtime = Date.now()
  returnfunction (...args) {
    let newtime = Date.now()
    if (newtime - oldtime >= delay) {
      fn.apply(null, args)
      oldtime = Date.now()
    }
  }
}
复制代码

使用定时器写法,delay毫秒后第一次执行,第二次事件停止触发后依然会再一次执行

functionthrottled2(fn, delay = 500) {
  let timer = nullreturnfunction (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args)
        timer = null
      }, delay)
    }
  }
}
复制代码

可以将时间戳写法的特性与定时器写法的特性相结合,实现一个更加精确的节流。实现如下

functionthrottled(fn, delay) {
  let timer = nulllet starttime = Date.now()
  returnfunction () {
    let curTime = Date.now() // 当前时间let remaining = delay - (curTime - starttime) // 从上一次到现在,还剩下多少多余时间let context = thislet args = argumentsclearTimeout(timer)
    if (remaining <= 0) {
      fn.apply(context, args)
      starttime = Date.now()
    } else {
      timer = setTimeout(fn, remaining)
    }
  }
}
复制代码

#防抖

简单版本的实现

functiondebounce(func, wait) {
  let timeout

  returnfunction () {
    let context = this// 保存this指向let args = arguments// 拿到event对象clearTimeout(timeout)
    timeout = setTimeout(function () {
      func.apply(context, args)
    }, wait)
  }
}
复制代码

防抖如果需要立即执行,可加入第三个参数用于判断,实现如下:

functiondebounce(func, wait, immediate) {
  let timeout

  returnfunction () {
    let context = thislet args = argumentsif (timeout) clearTimeout(timeout) // timeout 不为nullif (immediate) {
      let callNow = !timeout // 第一次会立即执行,以后只有事件执行后才会再次触发
      timeout = setTimeout(function () {
        timeout = null
      }, wait)
      if (callNow) {
        func.apply(context, args)
      }
    } else {
      timeout = setTimeout(function () {
        func.apply(context, args)
      }, wait)
    }
  }
}
复制代码

#二、区别

相同点:

  • 都可以通过使用 setTimeout 实现

  • 目的都是,降低回调执行频率。节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能

  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

例如,都设置时间频率为 500ms,在 2 秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在 2s 后,只会执行一次

如下图所示:

#三、应用场景

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求

  • 手机号、邮箱验证输入检测

  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听

  • 搜索框,搜索联想功能

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值