JS高频面试题整理(包含ES5,ES6)


文章目录


写一下 Javascript 的原始类型

原始类型:number,string,boolean,null,undefined

引用类型:object。里面包含function,Array,Date

Typeof null // 输出object(null 被认为是对象的占位符)

例举3种强制类型转换和2种隐式类型转换?

强制(parseInt, parseFloat, number)
隐式主要发生在 +、-、*、/ 以及 ==>< 这些运算符之间

parseInt() 函数可解析一个字符串,并返回一个整数。
parseFloat() 函数可解析一个字符串,并返回一个浮点数。
Number() 函数把对象的值转换为数字。

注意使用parseFloat()时:
1.字符串中只返回第一个数字。
2. 开头和结尾的空格是允许的。
3. 如果字符串的第一个字符不能被转换为数字,那么 parseFloat() 会返回 NaN。

列举鼠标事件。

onClick 鼠标点击事件
onDblClick 双击事件
onMouseOver 鼠标经过
onMouseOut 鼠标移出
onMouseDown 鼠标上的按钮被按下了
onMouseUp 鼠标按下后,松开时激发的事件

表单相关事件
onFocus 获得焦点
onBlur 失焦
onChange 改变文本框内容触发
onSubmit 一个表单被提交时触发的事件

JS中提取字符串的方法

substring(start,end)表示从start到end之间的字符串,包括start位置的字符但是不包括end位置的字符。

var src="images/off_1.png";
alert(src.substring(7,10));//off

字符串实现倒序输出

newStr = str.split('').reverse().join('')

如何提取高度嵌套的对象里的指定属性?

取对象里面的属性我们通常用解构赋值。但是有时候会遇到一些嵌套层级非常深的对象。

 const school = {
      classes: {
        stu: {
          name: '高度嵌套的铁锤',
          age: 18
        }
      }
    }

现在想取name属性值。可以在解构出来的变量名右侧,通过冒号 +{目标属性名} 书写形式,进一步解构它,一直解构拿到目标数据为止。

 const { classes: { stu: { name }}} = school
 console.log(name) // 高度嵌套的铁锤

数组相关

数组方法pop() push() unshift() shift()

push() 数组尾部添加
pop() 尾部删除
unshift() 数组头部添加
shift() 头部删除

数组的截取和合并

concat() 合并数组

var arr = [1,2,3];
var arr1 = [4,5,6];
var arr2 = arr.concat(arr1);
console.log(arr2); // [1,2,3,4,5,6];

slice() 来截取数组。

var arr = [1,2,3];
console.log(arr.slice(0,1);); // [1];
// slice方法接受两个参数,一个从哪里开始,一个是到哪里结束(但是不包括这个结束的元素本身)。如果是负数,就从倒数第几个。

删除指定数组

splice()从数组中添加/删除项目,然后返回被删除的项目

var arr = [1,2,3];
arr.splice(0,1); //从数组下标0开始,删除1个元素
console.log(arr); // [2,3];
// 接受两个参数,第一个是index,数组开始的下标,第二个是len,要删除的长度

数组元素的排序

reverse()颠倒数组中元素的顺序

let a = [1,2,3];
a.reverse(); 
console.log(a); // [3,2,1]

sort()方法对数组元素进行排序,并返回这个数组。
sort排序常见用法:数组元素为数字的升序、降序:

var array = [10, 1, 3, 4,20,4,25,8];
array.sort(function(a,b){
 return a-b; //升序
});
console.log(array); // [1,3,4,4,8,10,20,25];
var array = [10, 1, 3, 4,20,4,25,8];
array.sort(function(a,b){
 return b-a; //降序
});
console.log(array); // [25,20,10,8,4,4,3,1];

数组转字符串

join()方法用于把数组中的所有元素通过指定的分隔符进行分隔放入一个字符串,返回生成的字符串

let a= ['hello','world'];
let str=a.join(); // 'hello,world'
let str2=a.join('+'); // 'hello+world'

关于数组具体实例可参考 js中数组的操作方法

数组去重?

此题看着简单,但要想面试官给你高分还是有难度的。至少也要写出几种方法
js

var array=['12','32','89','12','12','78','12','32'];
    // 最简单数组去重法
    //新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,
    //如果有相同的值则跳过,不相同则push进数组。
    function unique1(array){
        var n = []; //一个新的临时数组
        for(var i = 0; i < array.length; i++){ //遍历当前数组
            if (n.indexOf(array[i]) == -1)
                n.push(array[i]);
        }
        return n;
    }
    arr=unique1(array);
    // 速度最快, 占空间最多(空间换时间)
    function unique2(array){
        var n = {}, r = [], type;
        for (var i = 0; i < array.length; i++) {
            type = typeof array[i];
            if (!n[array[i]]) {
                n[array[i]] = [type];
                r.push(array[i]);
            } else if (n[array[i]].indexOf(type) < 0) {
                n[array[i]].push(type);
                r.push(array[i]);
            }
        }
        return r;
    }
    //数组下标判断法
    function unique3(array){
        var n = [array[0]]; //结果数组
        for(var i = 1; i < array.length; i++) { //从第二项开始遍历
            if (array.indexOf(array[i]) == i) 
                n.push(array[i]);
        }
        return n;
    }

es6 Set()

es6方法数组去重,第一种方法
let arr = [1,2,3,4,2,1,2,3,4,12,3,1,2,31,1]
let s = [...new Set(arr)];
es6方法数组去重,第二种方法
function dedupe(array) {
  return Array.from(new Set(array));       //Array.from()能把set结构转换为数组
}

使用 filter 方法

// 参数self表示(调用 filter 方法的数组本身)
// self.indexOf(value) 可以得到当前元素在数组中第一次出现的索引,当它与当前索引 index 相等时,表示当前元素是第一次出现,保留;
// 否则,表示当前元素已经在之前出现过,进行过滤。
//filter 方法,最终返回一个新的数组,新数组中只包含满足条件的元素,即去除了重复的元素。
const arr = [1, 2, 3, 3, 4, 4, 5];
const uniqueArr = arr.filter((value, index, self) => {
  return self.indexOf(value) === index;
});
console.log(uniqueArr); // [1, 2, 3, 4, 5]

使用 reduce 方法

// prev 参数表示累加器(初始值为一个空数组),cur 参数表示当前元素。
// 通过判断当前元素 cur 是否已经存在于累加器 prev 中,如果不存在,则将其添加到累加器中。
// 这里利用了 includes 方法来检查元素是否已经存在于数组中。
const arr = [1, 2, 3, 3, 4, 4, 5];
const uniqueArr = arr.reduce((prev, cur) => {
  if (!prev.includes(cur)) {
    prev.push(cur);
  }
  return prev;
}, []);
console.log(uniqueArr); // [1, 2, 3, 4, 5]

使用 map 方法

// 创建一个空的 Map 对象 map,用于存储已经出现过的元素。然后,通过 forEach 方法遍历原数组 arr 的每个元素 item。
// 在遍历过程中,使用 map.has(item) 来检查 Map 中是否已经存在该元素。如果不存在,则说明该元素是第一次出现,将其作为键存储到 Map 中,并将其添加到新数组 uniqueArr 中
const arr = [1, 2, 3, 3, 4, 4, 5];
const map = new Map();
const uniqueArr = [];
arr.forEach(item => {
  if (!map.has(item)) {
    map.set(item, true);
    uniqueArr.push(item);
  }
});
console.log(uniqueArr); // [1, 2, 3, 4, 5]

JS中循环遍历数组的方法

JS中循环遍历数组的几种常用方式总结

数组扁平化

  let flatArr = [1, 2, 3, [[4, 5], 6], 7, 8, 9]

1. 调用ES6中的flat方法

flatArr = flatArr.flat(Infinity);

2. 普通递归

  function flatFun2(arr: any[]) {
    let result: number[] = []
    for (let i = 0; i < arr.length; i++) {
      if (Array.isArray(arr[i])) {
        result = result.concat(flatFun2(arr[i]))
      } else {
        result.push(arr[i])
      }
    }
    return result
  }

3. 利用reduce函数迭代

  function flatFun3(arr: any[]): number[] {
    return arr.reduce((prev, i) => {
      return prev.concat(Array.isArray(i) ? flatFun3(i) : i)
    }, [])
  }

4. 扩展运算符

  function flatFun5(arr: any[]): any[] {
    // 只要有一个元素有数组,那么循环继续
    while (arr.some(Array.isArray)) {
      arr = [].concat(...arr)
    }
    return arr
  }

可参考:第十篇: JS中flat—数组扁平化

对类数组的理解,如何转化成数组?

所谓类数组,当一个对象具有以下特点:

  • 可以使用 索引 对数据进行操作
  • 具有 callee 和 length 属性
  • 原型链指向Object,没有继承自 Array 原型链上的方法和属性。所以不能使用数组原型方法,如push、unshift等。

常见的类数组还有:

  • arguments
  • 用getElementsByTagName/ClassName()获得的HTMLCollection
  • 用querySelector获得的nodeList
let arrayLike = {
  0: 'apple',
  1: 'banana',
  2: 'orange',
  length: 3
};

console.log(arrayLike[0]); // 输出:'apple'
console.log(arrayLike.length); // 输出:3
console.log(Array.isArray(arrayLike)); // 输出:false
// 通过 Array.from 方法转换
Array.from(arrayLike)
// 通过 call 调用数组的 slice 方法实现转换
Array.prototype.slice.call(arrayLike)
// 通过 apply 调用数组的 concat方法实现转换
Array.prototype.concat.apply([],arrayLike)
 //声明一个空数组,遍历伪数组,将伪数组中的元素添加到真数组中**
  var arr=[];
  for(var i = 0;i < arrayLike.length; i++){
      arr.push(weiArr[i]);
   }

判断是否是数组的方式有哪些?

1)通过 Object.prototype.toString.call() 判断

Object.prototype.toString.call(obj).slice(8,-1)

2)原型链判断

obj._proto_ = Array.prototype

3)es6的Array.isArray()

Array.isArray(obj)

4) instanceof

obj instanceof Array

如何获取两个数组之间不同的元素

可以使用 JavaScript 中的 filter 方法和 includes 方法来获取两个数组之间不同的元素。

const filterArr = (arr1, arr2) => {
  const arr = [...arr1, ...arr2];
  const newArr = arr.filter((t) => {
    return !(arr1.includes(t) && arr2.includes(t));
  });
  return newArr;
};

数组里有两个一样的数字,如何找到?

  // 排序法
  // 先对数组进行排序,然后检查相邻的元素是否相等。时间复杂度取决于排序算法的性能,一般为 O(n log n)。
  function testArr(arr: any[]) {
    let result = [] as number[]
    arr.sort()
    for (let i = 0; i < arr.length; i++) {
      if (arr[i] === arr[i + 1]) {
        result.push(arr[i])
      }
    }
    return Array.from(new Set(result))
  }
    onMounted(() => {
    let array = [1, 2, 5, 4, 7, 9, 2, 4, 4]
    const result = testArr(array)
    console.log(result)  // 输出 [2,4]
  })
// 使用对象来实现
 function testArr(arr: any[]) {
    let obj = {}
    let newArr = [] as any[]
    for (let i = 0; i < arr.length; i++) {
      const num = arr[i]

      if (obj[num]) {
        newArr.push(num)
      } else {
        obj[num] = true
      }
    }
    return Array.from(new Set(newArr))
  }
  onMounted(() => {
    let array = [1, 2, 5, 4, 7, 9, 2, 4, 4]
    const result = testArr(array)
    console.log(result)  // 输出 [2,4]
  })
  // 集合唯一法
  // 使用集合(Set)来存储已经出现过的数字,遍历数组时,判断当前数字是否已经存在于集合中。时间复杂度为 O(n)。
  function testArr(arr: any[]) {
    let newArr = [] as any[]
    let set = new Set()

    for (let num of arr) {
      if (set.has(num) && !newArr.includes(num)) {
        newArr.push(num)
      } else {
        set.add(num)
      }
    }
    console.log(set, 'set')
    return newArr
  }

  onMounted(() => {
    let array = [1, 2, 5, 4, 7, 9, 2, 4, 4]
    const result = testArr(array)
    console.log(result)
  })

JS实现找出数组中重复的数字的三种方法

js 数组 reduce 方法的使用

介绍 reduce
reduce() 方法接受一个回调函数和一个可选的初始值作为参数,对数组中的每个元素依次调用回调函数,并将上一次调用的结果传递给下一次调用,最终返回一个累积的结果。

array.reduce(callback, initialValue)
  • 其中,callback 是一个函数,它可以接受四个参数:accumulator(累加器)、currentValue(当前值)、currentIndex(当前索引)和 array(当前数组)。 initialValue 是可选的初始值,
  • 如果提供了初始值,则作为第一次调用回调函数时的 accumulator 值;
  • 如果没有提供初始值,则使用数组的第一个元素作为初始值,并从第二个元素开始调用回调函数。

reduce 的使用

  // 累加
  const reduceArr1 = [1, 2, 3, 4].reduce((pre, cur) => {
    return pre + cur
  }, 0)
  console.log(reduceArr1, 'reduceArr1')

  // 找最大值 - 在每次调用回调函数时,我们比较累加器和当前值的大小,然后返回较大的那个值作为下一次调用的累加器值。
  // 这样,经过整个数组的遍历,累加器中就会包含数组中的最大值。
  const reduceArr2 = [1, 2, 3, 4].reduce((pre, cur) => {
    return Math.max(pre, cur)
  })
  console.log(reduceArr2, 'reduceArr2')

  // 去重
  const reduceArr3 = [1, 1, 2, 3, 4, 4, 5, 6].reduce((preList, cur) => {
    if (!preList.includes(cur)) {
      preList.push(cur)
    }
    return preList
  }, [])
  console.log(reduceArr3, 'reduceArr3')

  // 数组反转
  const reduceArr4 = [1, 2, 3, 4, 5].reduce((preList, cur) => {
    preList.unshift(cur)
    return preList
  }, [])
  console.log(reduceArr4, 'reduceArr4')

JS判断数组中是否包含某个值

Array.indexOf()、Array.includes()、Array.find()、Array.findIndex()

JS数组的高阶函数

1. 什么是高阶函数?

一个函数就可以接收另一个函数作为参数或者返回值为一个函数,这种函数就称之为高阶函数。

那对应到数组中有哪些方法呢?

2. 数组中的高阶函数

1)map

  • 参数:接受两个参数,一个是回调函数,一个是回调函数的this值(可选)。
    其中,回调函数被默认传入三个值,依次为当前元素、当前索引、整个数组。
  • 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
  • 对原来的数组没有影响。
let nums = [1, 2, 3];
let obj = {val: 5};
let newNums = nums.map(function(item,index,array) {
  return item + index + array[index] + this.val; 
  //对第一个元素,1 + 0 + 1 + 5 = 7
  //对第二个元素,2 + 1 + 2 + 5 = 10
  //对第三个元素,3 + 2 + 3 + 5 = 13
}, obj);
console.log(newNums);//[7, 10, 13]

当然,后面的参数都是可选的 ,不用的话可以省略。

2)filter

  • 参数: 一个函数参数。这个函数接受一个默认参数,就是当前元素。这个作为参数的函数返回值为一个布尔类型,决定元素是否保留。

filter方法返回值为一个新的数组,这个数组里面包含参数里面所有被保留的项。

let nums = [1, 2, 3];
// 保留奇数项
let oddNums = nums.filter(item => item % 2);
console.log(oddNums); // [1, 3]

3)reduce

  • 参数: 接收两个参数,一个为回调函数,另一个为初始值。回调函数中四个默认参数,依次为积累值、当前值、当前索引和整个数组。
let nums = [1, 2, 3];
// 多个数的加和
let newNums = nums.reduce(function(preSum,curVal,currentIndex,array) {
  return preSum + curVal; 
}, 0);
console.log(newNums);//6

不传默认值会自动以第一个元素为初始值,然后从第二个元素开始依次累计。

4)sort

  • 参数:一个用于比较的函数,它有两个默认参数,分别是代表比较的两个元素。
let nums = [2, 3, 1];
//两个比较的元素分别为a, b
nums.sort(function(a, b) {
  if(a > b) return 1;
  else if(a < b) return -1;
  else if(a == b) return 0;
})

当比较函数返回值大于0,则 a 在 b 的后面,即a的下标应该比b大。

反之,则 a 在 b 的后面,即 a 的下标比 b 小。

整个过程就完成了一次升序的排列。

当然还有一个需要注意的情况,就是比较函数不传的时候,是如何进行排序的?

答案是将数字转换为字符串,然后根据字母unicode值进行升序排序,也就是根据字符串的比较规则进行升序排序。

参考:第十一篇: JS数组的高阶函数——基础篇

关于for …of 和 for …in 区别

for of 和 for in 是两种不同的循环语句,用于遍历可迭代对象枚举对象的属性,它们的区别如下:

1)遍历的对象类型不同

  • for of 用于遍历可迭代对象,如数组、字符串、Set、Map等;不会遍历原型链。
  • for in 主要遍历对象,不适用遍历数组;会遍历对象的整个原型链,性能差一些。

2)遍历获取的内容不同

  • for of 遍历获取的是对象的键值,没有索引。
  • for in 遍历获取的是对象的键名(属性名)。

3)遍历顺序不同

  • for of 按照迭代器对象定义的顺序,从前往后依次遍历。
  • for in 遍历对象属性名时,顺序不确定,可能是随机的。(这也是不推荐用在数组上的原因之一)

4)可以使用的对象类型不同

  • for of 适用于可迭代对象,如数组、字符串、Set、Map等;
  • for in 适用于所有对象,包括普通对象、数组、字符串等。

注意:普通对象用 for of 遍历会报错。如果是类数组对象的话可以用Array.from() 转成数组再遍历。

  let forObject = Array.from({ 0: 'one', 1: 'two', 2: 'three', length: 3 })
  for (let item of forObject) {
    console.log(item, 'item')
  }

IE和标准下有哪些兼容性的写法

var ev = ev || window.event
document.documentElement.clientWidth || document.body.clientWidth
Var target = ev.srcElement||ev.target

假如让你设计一个适配 PC、手机和平板的项目,你有哪些布局方案?

首先是vh、vw,然后用淘宝出品的 lib-flexible 库进行 rem 适配,还有一种 flex + px 的适配方式。

什么是事件冒泡和事件捕获,如何阻止事件冒泡

事件冒泡事件捕获是两种不同的事件传播机制。

事件冒泡是指当一个元素触发(或接受到)某个事件时,该事件会从最内层的元素向父元素一级一级地传播,直到传播到最外层的元素。在事件冒泡过程中,事件会经过每个祖先元素,父元素会先接收到该事件,然后是祖父元素,依次向上。
事件捕获是指当一个元素触发(或接受到)某个事件时,该事件会从最外层的元素开始,逐级向内层元素传播,直到传播到最内层的元素。在事件捕获过程中,事件会经过每个祖先元素,最内层的元素会先接收到该事件。

阻止事件冒泡,可以使用事件对象的stopPropagation()方法。这个方法可以阻止事件继续向上层元素传播,即停止事件在 DOM 树上的冒泡过程。例如:

element.addEventListener('click', function(event) {
  event.stopPropagation();
});

需要注意的是,阻止事件冒泡只会影响到父级元素的事件监听器,不会影响当前元素本身上的其他事件监听器。

事件委托是什么??

利用事件冒泡的原理,让自己的所触发的事件,让他的父元素代替执行!

事件委托利用事件冒泡原理来实现,何为事件冒泡?就是事件从最深节点开始,然后逐步向上传播事件,比如页面有一个节点树,div > ul > li > a, 给最里面a加一个click点击事件,那么这个事件就会一层层往外执行,执行顺序 a > li> ul > div,既然有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div,所以都会触发,这就是事件委托,委托他们的父系级代为执行事件。

当有大量的子元素需要触发相同事件时,为每个子元素都绑定事件处理程序可能会导致性能问题。这时候可以利用事件委托,将事件处理程序绑定到共同的父元素上,通过事件冒泡机制捕获并处理子元素上的事件,例如:

var parent = document.getElementById('parentElement');
parent.addEventListener('click', function(event) {
  var target = event.target;
  if (target.tagName === 'BUTTON') {
    console.log('Button clicked!');
  }
});

在这个例子中,父元素parentElement上绑定了click事件处理程序,当用户点击子元素button时,事件会冒泡到父元素,并在父元素上触发click事件处理程序。通过判断event.target来确定用户点击的是哪个子元素。
事件委托的优点是减少了事件处理程序的数量,提高了性能,并且能够动态地处理新增的子元素。

如何阻止默认事件?

1.return false; 2.event.preventDefault();

//return false; 事件处理过程中,阻止了事件冒泡,也阻止了默认行为
<script type="text/javascript">
$(function() {
  $("#hr_three").click(function(event) {
    return false;
  });
});
<script>
//event.preventDefault(); 事件处理过程中,不阻击事件冒泡,但阻击默认行为
<script type="text/javascript">
$(function() {
  $("#hr_three").click(function(event) {
    event.preventDefault();
  });
});
<script>

Javascript的事件流模型都有什么?

先解释下事件流是什么:当你在页面触发一个点击事件后,页面上不仅仅有一个元素响应该事件而是多个元素响应同一个事件,因为元素是在容器中的。事件发生的顺序就是事件流,不同的浏览器对事件流的处理不同。

“事件冒泡”:事件开始由最具体的元素接收,然后逐级向上传播
“事件捕捉”:事件由最不具体的节点先接收,然后逐级向下,一直到最具体的
“DOM事件流”:三个阶段:事件捕捉,目标阶段,事件冒泡

添加 删除 替换 插入到某个接点的方法?

(1)创建节点
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点

(2)添加、移除、替换、插入
appendChild() //最后1个子节点之后添加一个新子节点
removeChild() //删除子节点
replaceChild() //替换子节点
insertBefore() //在已有的子节点前插入一个新节点

(3)查找
getElementById() //通过元素Id,唯一性
getElementsByTagName() //通过标签名称
getElementsByName() //通过元素的Name属性的值

遍历A节点的父节点下的所有子节点

这题考查原生的js操作dom,属于非常简单的基础题,但长时间使用mvvm框架,可能会忘记

<script>
    var b=document.getElementById("a").parentNode.children;
    console.log(b)
</script>

document load 和document ready的区别?

document.onload 是在结构和样式,外部js以及图片加载完才执行js。
document.ready是dom树创建完成就执行的方法,原生种没有这个方法,jquery中有 $().ready(function)

看下面代码,给出输出结果。

for(var i = 0; i < 5; i++){  //建议使用let 可正常输出i的值
  setTimeout(function(){
      console.log(i);   
  },0); 
};
//答案:5 5 5 5 5 
//原因:由于 JavaScript 中的事件循环机制,setTimeout() 并不会立即执行,而是会在当前执行栈的任务全部执行完毕后才会执行,因此打印的结果会是 5 个 5。
//如果想要按照预期输出 0、1、2、3、4,可以使用 let 关键字来定义变量 i,因为 let 会创建块级作用域,使得每个循环的 i 变量都有自己的作用域。

回答以下代码,alert的值分别是多少?

<script>
     var a = 100;  
     function test(){  
        alert(a);  
     a = 10;  //去掉了var 就变成定义了全局变量了
        alert(a);  
}  
test();
alert(a);
</script>
//正确答案是: 100, 10, 10

具体可查询:js变量的作用域详解

以下代码执行结果?

var uname = 'jack'
function change() {
    alert(uname) // ?
    var uname = 'lily'
    alert(uname)  //?
}
change()
//分别alert出 undefined,lily,(变量声明提前问题)

手写sleep函数(使用Promise封装setTimeout结合async/await)

function sleep(ms) {
  return new Promise(resolve =>
   setTimeout(resolve, ms)
   )}

// 调用函数
async function example() {
  console.log('开始执行');
  await sleep(2000); // 暂停2秒
  console.log('2秒后继续执行');
}

example();

使用setTimeout实现倒计时,和使用setInterval的区别?

setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是 每隔一段时间将事件加入事件队列中去,只有当执行栈为空的时候,才能去事件队列中取出事件执行。所以可能出现这样的情况,就是当前执行栈执行时间很长,导致事件队列里面积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能是间隔一段时间执行的效果。(间隔时间不准确)

针对 setInterval 这个缺点,我们可以使用 setTimeout递归调用 来模拟 setInterval ,这样我们就确保了只有一个事件结束了,才会触发下一个定时器事件,解决了 setInterval 的问题。(保证任务间的间隔时间为1秒)

setInterval 中当任务执行时间大于任务间隔时间,会导致任务挤压越来越多,处理不好导致内存崩溃。

实现思路: 使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果。

  // setTimeout实现倒计时
  function countDown(count) {
    setTimeout(() => {
      count--
      console.log(count, 'setTimeout 倒计时')
      if (count > 0) {
        countDown(count)
      }
    }, 1000)
  }
  countDown(10)
  // setInterval实现倒计时 - 需手动清除定时器
  function setIntervalFun() {
    let count = 10
    let timer = setInterval(() => {
      count--
      console.log(count, 'setInterval 倒计时')
      if (count <= 0) {
        clearInterval(timer)
        timer = null
      }
    }, 1000)
  }

  setIntervalFun()

对作用域、作用域链的理解

作用域:作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期

JavaScript 的作用域可以分为以下四种:

1)全局作用域

  • 最外层层函数和最外层函数外面定义的变量拥有全局作用域。
  • 所有未定义直接赋值的变量自动声明为全局作用域。
  • 所有window对象的属性拥有全局作用域。
  • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。

2) 函数作用域

  • 函数内声明的变量,只能在函数作用域范围内访问。
  • 内层作用域可以访问外层作用域,而外层作用域不能访问内层作用域。

3)块级作用域

  • 块级作用域可通过 let 和 const 声明,声明后的变量在指定作用域外无法被访问,块级作用域可以在函数中创建也可以在一个代码块中创建,由 { } 包裹。
  • let 和 const 声明的变量不会变量提升,同一作用域内,也不可以重复声明。

作用域链

  • 当所需要的变量在自己作用域中查找不到的时候,它会一层层向上查找,直到找到全局作用域(window对象)还没有找到的时候,就会放弃查找。这种一层层的关系,就是作用域链。
  • 总的来说,作用域查找变量的链条称为作用域链;作用域链是通过词法作用域来确定的,而词法作用域反映了代码的结构。
  • 在 JavaScript 执行过程中,其作用域链由词法作用域而不是调用栈决定的

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

对JavaScript 执行上下文的理解

当一段 JavaScript 代码在运行的时候,它实际上是运行在执行上下文中。下面 3 种类型的代码会创建一个新的执行上下文:

  • 全局执行上下文:任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象;一个程序中只能有一个全局执行上下文。
  • 函数执行上下文:当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。可以有多个函数执行上下文。
  • eval 函数:当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。不过eval 函数不常使用。

每个代码段开始执行的时候都会创建一个新的上下文来运行它,并且在代码退出的时候销毁掉。JavaScript 引擎通过创建 执行上下文栈 管理执行上下文。
也就是说在执行 JavaScript 时,可能会存在多个执行上下文,那么 JavaScript 引擎是如何管理这些执行上下文的呢?答案是通过一种叫的数据结构来管理的。

执行上下文栈

  • JavaScript 引擎通过 创建执行上下文栈 管理执行上下文。
  • 执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。
  • 当 JavaScript 执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
  // 执行上下文栈 先把 first加入执行栈,遇到second再加入执行栈,遵循后进先出的原则, 所以执行顺序 second() =》 first()
  let a = 'hello world!'
  function first() {
    console.log('inside first function')
    second()
    console.log('again inside first function')
  }
  function second() {
    console.log('inside second function')
  }
  first()

总之关于执行上下文就是:

1)因为JavaScript 代码的执行流程有两个阶段:编译阶段执行阶段。所以在执行 JS代码 之前,需要先编译代码。编译时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为 undefined,函数先声明好可使用。
2)经过编译后,会生成两部分内容:执行上下文可执行代码。这一步执行完了,开始正式的执行程序。

3)在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出 this、arguments和函数的参数列表。

  • 全局上下文:变量定义、函数声明
  • 函数上下文:变量定义、函数声明、this、arguments、参数列表

浏览器如何运行一段JavaScript代码

引用网络图片,整个流程图如下:

在这里插入图片描述
在这里插入图片描述

  1. 解析器将 JavaScript 代码解析(词法分析、语法解析)成AST并创建执行上下文
  2. 解释器 Ignition 将AST转化为字节码(字节码是介于AST和机器码之间的一种代码,字节码需要解释器将其转化为机器码之后才能执行。)
  3. 解释器 Ignition 对字节码逐条解释执行
    如果发现热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。

整个JavaScript代码执行是慢启动,越执行越快。这种字节码配合解释器和编译器的技术叫做即时编译(JIT)。v8引擎也是基于这种技术来实现对内存占用和执行效率的调控

具体解析请看:浏览器如何运行一段JavaScript代码

如何获取javascript三个数中的最大值和最小值??

Math.max(a, b ,c) //最大值
Math.min(a, b ,c) //最小值

程序中捕获异常的方法?

在这里插入图片描述

// try...catch只能捕捉到同步执行代码块中的错误,不能异步捕获代码错误
try{
 
}catch(e){
 
}finally{
 
}

可参考:因为一道try…catch的题,我的面试挂掉了

undefined 和 null 的区别

undefined 和 null 都是基本数据类型,undefined 表示未定义,null 表示空对象。一般变量声明了但还未定义的时候会返回 undefined,null主要用于复制给一些可能会返回对象的变量,作为初始化。

当对这两种类型使用 typeof 进行判断时,null 类型会返回“object”,这是个历史遗留问题。当使用 双等号 == 对两种类型进行比较时返回true,使用三个等号 === 会返回fasle。

console.log(null == undefined);    //true  因为两者都默认转换成了false
console.log(null === undefined);    //false   "==="表示绝对相等,null和undefined类型是不一样的,所以输出“false”
console.log(typeof undefined);    //"undefined"  
console.log(typeof null);       //"object"  

可以发现:null和undefined 两者相等,但是当两者做全等比较时,两者又不等。
原因:类型不一样
null: Null类型,代表“空值”,代表一个空对象指针,使用typeof运算得到 “object”,所以你可以认为它是一个特殊的对象值。
undefined: Undefined类型,当一个声明了一个变量未初始化时,得到的就是undefined。

那到底什么时候是null,什么时候是undefined呢???

null表示"空对象",即该处不应该有值。典型用法是:

(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。

undefined表示缺少值,即此处应该有值,但没有定义。典型用法是:

(1)定义了形参,没有传实参,显示undefined

function fn(num1, num2, num3) {
    return num3;
}
fn(10, 20); //undefined

(2)变量提升

console.log(num); //undefined
var num = 10;

(3)对象属性名不存在时,显示undefined

var obj = {
        name: '李四',
        age: 24
 }
console.log(obj.weight); //undefined

(4)定义变量,但未赋值

var a;
console.log(a); //undefined

(5)函数没有写返回值,即没有写return,拿到的是undefined

function fn() {
    var num = 1;
    console.log(num);//1
}
console.log(fn());//undefined

(6)写了return,但没有赋值,拿到的是undefined

function fn() {
    return;
}
console.log(fn()); //undefined

写一下 js的比较运算符,逻辑运算符

比较运算符:

>    大于
<    小于
>=   大于或等于
<=   小于或等于
==   等于
!=   不相等
===  值相等并且类型相等
!=== 值不相等或类型不相等

逻辑运算符:

在这里插入图片描述
比较运算符和逻辑运算符用于测试true和false

Object.is()、== 和 === 的区别

  • 使用双等号 == 进行相等判断时,如果两边类型不一致,会进行强制转化后进行比较。
  • 使用三等号 === 进行相等判断时,如果两边类型不一致,不做强制类型转化,返回false。
  • Object.is()在三等号严格相等的基础上修复了一些特殊情况下的失误,具体来说就是 -0 和 +0 不相等,NaN和NaN是相等的。 源码如下:

function is(x, y) {
  if (x === y) {
    //运行到1/x === 1/y的时候x和y都为0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一样的
    return x !== 0 || y !== 0 || 1 / x === 1 / y;
  } else {
    //NaN===NaN是false,这是不对的,我们在这里做一个拦截,x !== x,那么一定是 NaN, y 同理
    //两个都是NaN的时候返回true
    return x !== x && y !== y;
  }

其它判断案例:

 console.log(1==='1') // false, 因为前面的1是数字类型,后面的1是字符串类型
 console.log([1,23] == [1,23]) // false,因为都是引用类型,指向的地址值不一样
 console.log("" == []) //true,空字符串和数组都被强制转换成了数值0

// ===
console.log(0 === -0); // true
console.log(NaN === NaN); // false

// Object.is 
console.log(Object.is(0, -0)); // false
console.log(Object.is(NaN, NaN)); // true

[] == ![] 结果是什么?为什么?

== 中,左右两边都需要转换为数字,然后进行比较;
左边的 [] ,转换为数字0;
右边的 ![],首先转换为布尔值,由于 [] 作为一个 引用类型 转换为布尔值为 true;
因此 ![] 为false,进而转换再转换成数字,变成0;
0 == 0,结果为 true。

在JavaScript中,将引用类型转换为布尔值时,几乎所有的引用类型都会被解析为true,包括空数组[]。

let、var、const的区别

  1. 作用域:使用 var 声明的变量属于全局变量或者函数作用域,即在声明它的函数内部有效,而使用 letconst 声明的变量属于块级作用域,即在声明它的语句块内部有效(例如,if、for、while、try 等语句块)。这意味着使用 var 声明的变量在函数外部也可以访问,而使用 let 声明的变量只能在语句块内部访问。
  2. 变量提升: 使用 var 声明的变量会被提升函数作用域的顶部,也就是说,即使在变量声明之前使用了变量,也不会报错,只是返回 undefined。而使用 letconst 声明的变量不会被提升,如果在声明之前使用变量,会抛出 ReferenceError。
  3. 暂时性死区:在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。
  4. 重复声明:在同一个作用域中,使用 var 声明同名的变量不会报错,而是会覆盖原来的变量。而使用 letconst声明同名的变量会报错
  5. 给全局添加属性:浏览器的全局对象是 window,Node 的全局对象是 global。var 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 letconst 不会。
  6. 初始值设置: 在变量声明时,varlet 可以不用设置初始值。而 const 声明变量必须设置初始值。
  7. 指针指向letconst 都是 ES6 新增的用于创建变量的语法。 let 创建的变量是可以更改指针指向(可以重新赋值)。但 const 声明的变量是不允许改变指针的指向。
    在这里插入图片描述

让我写一段代码,我用的 let 定义的变量,问为什么不用 const 定义呢?

在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。
const优于let有几个原因。

  • 一是阅读代码的人立刻会意识到不应该修改这个值。
  • 二是防止了无意间修改变量值所导致的错误。
  • 三是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率,

可参考: 前端面试题:JS中的let和var的区别

记录一个实际案例:

openPopup(){
  try {
     var cardId = this.cardId 
  } catch(error){
     this.$toast('该游戏暂不支持红包充值,请联系客服处理')
     return
  }
}

如果这里用let,那么这个变量必须写在try外面,才能让后面的代码调用。
var 是 函数作用域,这样写就可以少写一行。

let 与 const 异同

const 与 let 很类似,都具有上面提到的 let 的特性,唯一区别就在于 const 声明的是一个只读变量声明之后不允许改变其值。因此,const 一旦声明必须初始化,否则会报错。

let a;
const b = "constant"

a = "variable"
b = 'change' // TypeError: Assignment to constant variable

变量提升和函数提升的区别?

变量提升和函数提升是JavaScript中的两个重要概念,它们都是在代码执行前将声明的变量或函数提前到当前作用域的顶部

但是,它们之间有一个关键的区别:即函数声明会被整个提升到当前作用域的顶部,而变量声明只会被提升。

  1. 变量提升:在JavaScript中,所有声明的变量都会在代码执行前被提升到当前作用域的顶部,但是只有声明的变量会被提升,而变量的赋值不会被提升。例如:
console.log(a); // undefined,因为只是声明的变量被提升,赋值操作并没有被提升
var a = 1;
  1. 函数提升:与变量提升不同,函数的声明会被整个提升到当前作用域的顶部,并且函数的定义和函数名都会被提升。例如:
foo(); // "hello"
function foo() {
  console.log("hello");
}

如果变量名和函数名重名的话,优先级:函数提升 > 变量提升

下面的代码打印什么内容,为什么?

var b = 10;
(function b(){
    b = 20;
    console.log(b); 
})();
var b = 10;
(function b() {
   // 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
   // IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
  // (这里说的“内部机制”,想搞清楚,需要去查阅一些资料,弄明白IIFE在JS引擎的工作方式,堆栈存储IIFE的方式等)
    b = 20;
    console.log(b); // [Function b]
    console.log(window.b); // 10,不是20
})();

以下代码输出什么?

  const a = 1
  console.log(a.__proto__ === Number.prototype)  // true
  console.log(a instanceof Number)  // false

基础类型尝试访问_proto_会包装成 Number 类型进行装箱,而它本身是基础类型的。

问题补充:基础类型是否有proto ? 回答:没有

如果 new 一个箭头函数的会怎么样?

箭头函数是ES6中提出来的,它没有 prototype,也没有自己的 this 指向,没有自己的 arguments 对象,所以不能 new 一个箭头函数,控制台会报错。

new操作符的实现步骤如下:

  1. 首先创建一个空对象(创建一个新的存储空间)
  2. 将构造函数的作用域赋给新对象(也就是将对象的_proto_属性指向构造函数的 prototype 属性)
  3. 将构造函数中的 this 指向新对象,并执行构造函数代码(为新对象添加属性和方法)
  4. 返回新对象(所以构造函数不需要return)

所以,上面的第2、3步,箭头函数都是没有办法执行的。

手写 new 操作符

function myNew(constructor, ...args) {
  const obj = Object.create(constructor.prototype) // 创建一个新对象并链接到构造函数的原型
  const result = constructor.apply(obj, args) // 将构造函数的 this 指向新对象并执行构造函数
  return result instanceof Object ? result : obj // 确保构造函数返回一个对象,如果没有则返回新对象
}

function Person(name) {
  this.myName = name
}
const preson1 = myNew(Person, 'Alice')
console.log(preson1.myName, 'new preson1') // 输出Alice

普通函数和箭头函数的区别?

  • 箭头函数写法比普通函数写法更加简洁、醒目。
  • 箭头函数没有 this 对象,它会从自己的作用域链的上一层继承 this,也就是说箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的(因此箭头函数无法使用 apply / call / bind 改变 this 指向)
  • 箭头函数的 this 指向是在声明时确定的,即在创建函数时,this就被绑定到了创建时所处的上下文中。
  • 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 …rest 参数代替。
    arguments 是一个类数组,…rest是真数组。
  • 没有 prototype 属性,即指向 undefined;
  • 不可以使用 new 命令,因为:
    1)没有自己的 this,无法调用 call、apply,将 this 指向实例对象。
    2)没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 _proto_
  • 总之箭头函数没有 arguments ,不能使用 supernew.targrt,也不能用作构造函数。

什么是闭包?手写一个闭包函数?闭包作用以及优缺点?*

可参考:闭包的作用和优缺点。闭包作用这里讲的很详细!!!!!!

红宝书上对于闭包的定义:有权访问另外一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

作用:

1.读取函数内部的变量;
2.这些变量的值始终保持在内存中,不会在外层函数调用后被自动清除。

详细描述就是:
涉及到变量的生命周期问题了,函数内部定义的变量属于局部变量,局部变量的生命周期是:当它所在的函数被调用的时候,就是开始,当调用执行一旦结束,局部变量就会被释放,当我们需要函数内部变量时,他已经被释放了,读取不到了,这个时候怎么解决?我们就要想办法延长他的生命周期
闭包的目的也可以说就是这个,延长局部变量的生命周期,当函数执行完毕以后,局部变量不可以被内存释放,然后让外部可以访问到这个变量

闭包形成的条件:

 - 函数的嵌套
 - 内部函数引用外部函数的变量或函数,并把引用的变量保存到堆中(通过返回内部函数,可以让外部作用域中的变量继续存在,延长外部函数的变量生命周期)

优点:

优点:
1:变量长期驻扎在内存中
2.延长变量的生命周期

闭包有哪些表现形式?

闭包产生的本质就是,当前环境中存在指向父级作用域的引用;下面四种表现形式都脱离不了这个本质。

1. 返回一个函数。

function f1() {
  var a = 2
  function f2() {
    console.log(a);//2
  }
  return f2;
}
var x = f1();
x();

也可以是下面这种,也属于闭包。

var f3;
function f1() {
  var a = 2
  f3 = function() {
    console.log(a);
  }
}
f1();
f3();

2. 作为函数参数传递。

var a = 1;
function foo(){
  var a = 2;
  function baz(){
    console.log(a);
  }
  bar(baz);
}
function bar(fn){
  // 这就是闭包
  fn();
}
// 输出2,而不是1
foo();

3. 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
4. IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window当前函数的作用域,因此可以全局的变量。

var a = 2;
(function IIFE(){
  // 输出2
  console.log(a);
})();

闭包的坏处:

函数内部可以访问函数外部的变量,容易造成内存泄露,因为闭包中的局部变量永远不会被回收。
解决这个问题的办法就是在不使用这些变量时,及时把不需要的局部变量全部删除

闭包应用场景:

任何闭包的应用场景都离不开这两点:

  • 创建私有变量
  • 延长变量的生命周期
1. 封装私有变量和方法(环境保护)

使用闭包可以将一些私有的变量和方法隐藏在一个函数作用域内,只暴露出特定的接口给外界使用,从而实现封装。
这种方式可以有效地保护代码的安全性,避免外部直接访问和修改内部变量和方法。
例如,下面的代码通过闭包封装了一个计数器,只能通过返回的对象来操作计数器,而外部无法修改变量 count 的值:

function createCounter() {
  let count = 0;
  return {
    increment() {
      count++;
    },
    decrement() {
      count--;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.getCount()); // 输出 0
counter.increment();
console.log(counter.getCount()); // 输出 1

另外防抖节流函数方法也属于这属于私有变量,不污染其它代码。

2)缓存数据(计数器)

使用闭包可以实现简单的缓存功能,将一些计算结果缓存在闭包中。
闭包和函数内传入的值有关联。下面的代码中 createInc 函数调用后,返回一个箭头函数,箭头函数中的 index 和 startValue 是外部变量,箭头函数内可以拿到两个变量的值,并且可以对变量的值进行修改。

例如:

function createInc(startValue) {  
    let index = -1;  
    return (step) => {    
        startValue += step;    
        index++;    
        return [index, startValue];  
     };
}
const inc = createInc(5);
console.log(inc(2)); // [0, 7]
console.log(inc(2)); // [1, 9]
console.log(inc(2)); // [2, 11]
3)实现模块化

实现模块化的方式有很多种,其中一种常见的方式就是使用闭包
具体地,可以将需要隐藏的私有变量和方法放在一个自执行函数(立即调用函数表达式)的作用域中,并返回外部可访问的公共接口。可以有效组织代码,并且减少了全局变量的污染。

例如,下面的代码通过闭包实现了一个简单的打印模块:

const printer = (function() {
  function print(msg) {
    console.log(`[INFO] ${msg}`);
  }
  
  function warn(msg) {
    console.warn(`[WARN] ${msg}`);
  }
  
  function error(msg) {
    console.error(`[ERROR] ${msg}`);
  }
  
  // 暴露出特定的接口
  return {
    print,
    warn,
    error
  };
})();

printer.print('Hello, World!'); // 输出 [INFO] Hello, World!
4)循环内执行异步函数(经典场景)
for (let i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log('异步任务', i);
    }, 1000);
}

console.log('同步任务', i);
//执行顺序5 => 0,1,2,3,4 即第一个5直接输出,1秒后,输出0,1,2,3,4
5)立即执行函数
for (let i = 0; i < 5; i++) {
  (function(index) {
    setTimeout(function() {
      console.log('异步任务', index);
    }, 1000);
  })(i);
}
console.log('同步任务', i);  //输出结果为:  5 => 1秒钟后再输出 0,1,2,3,4

什么叫内存泄漏?除闭包之外,还知道哪些会导致内存泄漏?

在javaScript中,内存泄漏指的是程序分配的内存空间因为某些原因而无法被垃圾回收机制正确的释放。这通常是由于代码中持续引用着不需要的对象或者变量,或者存在未及时清除的定时器或者事件监听器等情况所致。如果内存泄漏问题长期存在,会导致浏览器卡顿或者崩溃,影响用户体验。

内存泄漏需满足两个前提条件:

  • 我确定不需要这块内存了
  • 无法被垃圾回收器回收

其他内存泄漏:

  1. 不规范的使用闭包:从而导致某些变量一直被留在内存当中。把不用的闭包相关数据(内存)释放 置为null。

  2. 没有清理定时器或回调函数:设置了 setInterval 定时器,忘记取消,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,无法被回收。

  3. 互相引用:如果两个对象相互引用,但是没有其他地方引用它们,那么它们就不能被垃圾收集器回收,导致内存泄漏。

  4. 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。

// bar 被添加到 window 对象上了,如果 bar 指向一个巨大的对象或 DOM 节点,那就是安全隐患。
function foo(arg) {
	bar = "this is a hidden global variable";
}
  1. 脱离 DOM 的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
var elements = {
	button: document.getElementById("button")
};
function doStuff() {
	button.click();
}
function removeButton() {
	// button 是 body 的子节点.
	document.body.removeChild(document.getElementById("button"));
	// 因为 elements 对象中缓存了 DOM 节点引用,这里我们始终有对 id 是 button 的引用
}

JS垃圾回收机制?JS垃圾回收方法?

JavaScript 的垃圾回收机制是自动垃圾回收,它的主要工作就是定期扫描内存中的对象并清除不再使用的对象所占用的内存空间,以避免内存泄漏和浪费。

具体来说,JavaScript 引擎会在变量不再有用时(即没有被任何其他变量或函数引用)将其标记为“可回收”对象。然后,垃圾回收器会定期扫描内存,找到这些可回收对象,并释放他们所占的内存空间。垃圾回收器采用的算法包括标记-清除算法引用计数算法分代回收算法等。

需要注意的是,垃圾回收器只能回收动态分配内存,而不能处理静态分配内存,例如全局变量和闭包等。因此,在编写 JavaScript 代码时,我们应该尽可能避免创建过多的全局变量和闭包,同时及时处理掉不再使用的变量和对象,以便垃圾回收器能够更好的完成垃圾回收工作。

可参考:简单了解 JavaScript 垃圾回收机制
深入理解JavaScript——垃圾回收机制

引用计数算法 和 标记清除算法

引用计数算法
顾名思义,让所有对象实现记录下有多少“程序”在引用自己,让各对象都知道自己的 人气指数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

优势:

  • 可即刻回收垃圾。当被引用数值为0时,变成垃圾就立刻被回收。
  • 不用去遍历堆里面的所有活动对象和非活动对象。

劣势:

  • 计数器需要占很大的位置。
  • 最大的劣势是无法解决对象互相引用无法回收的问题。因为算法是将 引用次数为 0 的对象销毁,此处都不为 0,是1,导致 垃圾回收器 不会回收他们,那么这就是 内存泄漏 问题。后来这个机制被放弃了。

在这里插入图片描述
标记-清除算法
这种垃圾回收方式是一个定时运行的任务,也就是说当程序运行一段时间后,统一垃圾回收;
在 V8 引擎里面,使用最多的就是这种方法。

该算法垃圾回收过程分为两个阶段

  • 标记阶段:把所有活动对象做上标记。
  • 清除阶段:把没有标记(也就是非活动对象)销毁

在这里插入图片描述
优势:
实现简单,打标记也就是打或者不打两种可能,所以就一位二进制位就可以表示解决了循环引用问题。
(这种方法可以解决循环引用问题,因为两个对象从全局对象出发无法获取。因此,它们无法被标记,他们将会被垃圾回收器回收)
劣势:
造成碎片化(有点类似磁盘的碎片化)
再分配时遍次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端

减少垃圾回收:
对于JS垃圾回收机制,当然是垃圾生成的越少越好,我们可以用一些方法减少垃圾回收,例如手动进行内存释放:

  • 对数组进行优化:在清空一个数组时,最简单的方法就是给其赋值为 [ ],但是与此同时会创建一个新的空对象,可以将数组长度设置为0,以此来达到清空数组的目的。
  • 对 object 进行优化:对象尽量复用,对于不再使用的对象,就将其设置为 null ,尽快被回收。
  • 对函数进行优化:在循环中的函数表达式,如归可以复用,尽量放在函数的外面、
let arr = [a,b,c,d,e,f];
arr.length = 0;        // 将长度设置为0释放内存
let obj = {
    name: "小明",
    age: 18,
}
obj = null;        // 清除引用释放内存
// 等等。。。

什么是回调函数?

  把a函数作为b函数的参数,传递给b函数,则这个a函数就叫做b的回调函数 
var a = function(){

}
var b = function(callback){

}
b(a); //把a函数作为参数传递给b函数,则a就是b的回调函数

什么是回调地狱:

  1. 多层嵌套的问题。
  2. 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。明显增加了代码的混乱程度。

这两种问题在回调函数时代尤为突出。Promise 的诞生就是为了解决这两个问题。

promise是什么?怎么使用??缺点?

  • Promise 是异步操作的一种解决方案,一般用来解决层层嵌套的回调函数(回调地狱)的问题。Promise 解决的是异步编码风格的问题,而不是一些其他的问题。
  • Promise 是一个构造函数,接受一个 函数 作为参数,返回一个 Promise 实例,因此可以对返回值进行 .then() 调用。
  • 每一个 .then()/.catch() 方法返回的是一个新生成的 Promise 对象,这个对象可被用作 链式调用。即 then() 方法后面再调用另一个 then() 方法。例子见下文。
  • Promise 本身是同步的立即执行函数,当执行 resolve/reject 的时候,此时是异步操作,会先执行then/catch等,当主线程执行完成后,才会去调用 resolve/reject 中存放的 方法执行。
  • Promise对象的错误具有“错误冒泡”机制,会一直向后传递,直到被捕获为止。例子见下文。

一个promise实例有三种状态: pendingfulfilledrejected。分别代表进行中、已成功和已失败。初始值是pending,实例的状态只能由 pending 转变 fulfilled 或者rejected。状态一旦改变,就不会再改变了。

当Promise启动后,
满足成功的条件时我们让状态从等待变成成功
满足失败的条件时我们让状态从等待变成失败

应用场景:

1、解决回调地狱 (通过.then .catch方法,处理ajax返回的结果,用链式调用的方式解决回调地狱)
2、将异步操作队列化,按照期望的顺序执行,返回符合预期的结果(.then(1).then(2).then(3) 顺序执行)

手写实现Promise使用:

  //拿ajax的例子来做一下Promise封装
  function myAjax(url) {
    return new Promise((resolve, reject) => {
      $.ajax({
        url: url,
        success: function(data) {
          //成功则返回data数据
          resolve(data)
        },
        error: function(err) {
          //失败则返回错误信息err
          reject(err)
        }
      });
    })
  }

  /*来来来,看好了,
  封装好ajax之后我们就可以去进行请求数据了*/
  let res1 = myAjax('xxxx/api/xxxx');
  let res2 = res1.then(myAjax('xxxx/api/xxxx'));
  let res3 = res2.then(myAjax('xxxx/api/xxxxx'));

当要写有顺序的异步事件时,需要串行时,可这样写:
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

 let promise =new Promise((resolve, reject) =>{
      ajax('first').success(function(res){
        resolve(res)
      })
    })
    promise.then(res =>{
      return new Promise((resolve, reject) =>{
        ajax('second').success(function(res){
          resolve(res)
      })
    })
  }).then(res =>{
    return new Promise((resolve, reject) =>{
        ajax('third').success(function(res){
          resolve(res)
      })
    })
  }).then(res =>{

  })

Promise对象的错误具有“错误冒泡”机制。

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 处理前面三个Promise产生的错误
});

缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

可参考:(前端必会)理解异步和Promise的使用
js promise看这篇就够了

async/await

async/await 出现的原因
Promise 的编程模型依然充斥着大量的 then 方法,虽然解决了回调地狱的问题,但是在语义方面依然存在缺陷,代码中充斥着大量的 then 函数,这就是 async/await 出现的原因。async/await 让代码更少,更简洁。

定义
根据 MDN 定义,async 是一个通过异步执行隐式返回 Promise 作为结果的函数。对 async 函数的理解,这里需要重点关注两个词:异步执行隐式返回 Promise

  • async/await 技术背后的秘密就是 Promise生成器应用,往低层说就是微任务协程应用

  • 建立在Promises之上,相对于 Promise 和回调,它的可读性和简洁度都更高

  • 它是基于promise构建的,比如async 定义的函数,返回的还是一个promise对象

  • 使用 async/await 可以实现用同步代码的风格来编写异步代码,这是因为 async/await 的基础技术使用了生成器Promise,生成器是协程的实现,利用生成器能实现生成器函数的暂停和恢复。

  • 另外,V8 引擎还为 async/await 做了大量的语法层面包装,比如 await 100,会默认创建一个 Promise 对象,代码如下所示:

let promise_ = new Promise((resolve,reject){
  resolve(100)
})

async/await 捕获异常

  async fn(){
    try {
      let a= await Promise.reject('error')
    } catch (error) {
      console.log(error)
    }
  }

可参考:vue中异步函数async和await的用法

async/await 对比Promise的优势

  • 解决问题的角度:Promise 解决了多个回调函数嵌套的时候会造成回调地狱问题,不利于代码维护。async/await 解决 Promise的多个 then 的链式调用问题。
  • 语法Promise 使用链式调用的方式处理异步操作,通过 thencatch 方法来注册回调函数。
    async/await 使用更直观的同步语法,使用 async 声明一个 function 是异步函数,然后通过 await 关键字等待一个异步方法执行完成,并且会阻塞当前函数体内后面的代码,等await等待的 promise对象执行完毕后,再执行阻塞的代码。规定await只能出现在async函数内。async 函数返回的是一个 Promise 对象。
  • 错误处理:在 Promise 中,使用 catch 方法来捕获和处理错误。而在 async/await 中,可以使用 try/catch 语句来捕获异步函数中的错误。
  • 可读性:相对于 Promise 的链式调用,async/await 更接近传统的同步编程风格,使得异步代码更易于理解和维护。
  • 异步操作的顺序控制:使用 Promise 时,你可以使用 .then 方法将多个异步操作串联起来,或者使用 Promise.all 来等待多个异步操作都完成。而使用 async/await,则可以使用 await 关键字按照顺序依次执行异步操作。

promise.all 和 promise.race 的区别

Promise.all

  • Promise.all 可将多个 Promise 实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功返回的是一个结果数组失败返回的是 第一个reject失败状态的值,而且并不影响其他Promise实例正常的reject拒绝操作。
  • Promise.all 传入数组中的多个 Promise 实例状态都变成 fulfilled,返回新的 Promise 实例状态才会变成 fulfilled,只要数组中有一个 Promise 实例 被rejected,返回新的 Promise 实例 就变成 rejected
  • Promise.all 传入的是数组,返回的也是数组,并会进行映射,传入的Promise对象返回的值是按照顺序在数组中排列的,注意他们的执行顺序并不是按照传入顺序的,除非可迭代对象为空。
  • Promise.all 获得的成功结果数组的数据顺序和 Promise.all 接收到的数组顺序是一致的,这样当遇到发送多个请求并根据顺序获取和使用数据的场景,就可以使用 Promise.all来解决。(用于并发请求)
  • 注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]

如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了

promise.race

  • Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

  • 字面意思 “赛跑”,就是说 promise.race([a1,a2,a3]) 里面哪个结果获得的快,就返回哪个结果,不管结果本身是成功还是失败。

  • Promise.race() 不会对resolve(解决)或reject(拒绝)的 Promise 区别对待。无论是解决还是拒绝,只要第一个落定的 Promise,Promise.race() 就会包装其解决值或拒绝理由并返回新Promise。

  • 当做一件事,超过多少长时间就不做了,可用 promise.race 来解决;比如可以设置图片请求超时。例子在下文

 Promise.race([testPromise(true), testAwt()]);
  async function runAsync(x: number) {
    const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
    return p
  }
  Promise.race([runAsync(1), runAsync(2), runAsync(3)])
    .then(res => {
      console.log('result', res)
    })
    .catch(err => {
      console.log('err', err)
    })
 // 1 => result 1 => 2 => 3
 // then 只会捕获第一个成功的方法,其他的函数虽然还会继续执行,但是不被then捕获了
//请求某个图片资源
function requestImg(){
    var p = new Promise(function(resolve, reject){
        var img = new Image();
        img.onload = function(){
           resolve(img);
        }
        //img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg"; 正确的
        img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg1";
    });
    return p;
}

//延时函数,用于给请求计时
function timeout(){
    var p = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('图片请求超时');
        }, 5000);
    });
    return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
    console.log(results);
})
.catch(function(reason){
    console.log(reason);
});

说一下同步和异步

同步会阻塞,异步不会阻塞

同步:程序运行从上而下,浏览器必须把这个任务执行完毕,才能继续执行下一个任务

异步:程序运行从上而下,浏览器任务没有执行完,但是可以继续执行下一行代码,当被调用者得到结果之后会通过回调函数主动通知调用者。

JS中的异步操作有哪些

1、定时器都是异步操作
2、事件绑定都是异步操作
3、AJAX中一般我们都采取异步操作(也可以同步)
4、回调函数可以理解为异步(不是严谨的异步操作)
剩下的都是同步处理

谈谈你对原型链的理解

1. 原型对象和构造函数有何关系?

  • 在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象 。可以控制台打印console.log(函数名fn.prototype)
  • 每当函数经过 new 调用时,这个函数就成了构造函数,返回一个全新的实例对象,这个实例对象有一个 _proto_ 属性,指向构造函数的原型对象(构造函数在定义时首字母需要大写)

在这里插入图片描述

2. 能不能描述一下原型链?

  • 当访问某个对象的属性时,会先在这个对象本身属性上查找,如果没有找到,就去它的原型(_proto_)去找,即它的构造函数的prototype查找,如果没有找到,就到原型的原型去找(构造函数的prototype._proto_)。如果直到最顶层的 Object.prototype 还是找不到,是null,则返回undefined。这样一层一层的查找就会形成一个链式结构,这就是原型链
  • 总之,JavaScript 对象通过 _proto_ 指向父类对象,_proto_ 就是我们原型链中的 连接点;直到指向Object对象为止,这样就形成了一个原型指向的链条,即原型链。
  • 对象的 hasOwnPrototype() 来检查对象自身中是否含有该属性。
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,会继续检查原型链,也会返回true。

在这里插入图片描述

详细文章请看 浅析js中的原型和原型链及其使用场景

手写代码实现 instanceof (考察对原型链的理解)

  • 步骤1:先取得当前类的原型,当前实例对象的原型链
  • 步骤2:一直循环(执行原型链的查找机制)
    获取当前实例对象原型链的原型链(proto = proto. _proto_,沿着原型链一直往上找)
    如果 当前实例的原型链 _proto_ 上找到了当前类的原型 prototype,则返回true
    如果 一直找到 object.prototype._proto_ == null,object的基类 null 上面都没找,就返回false
  // 实例._proto_ === 构造函数.prototype
    function _instanceof (instance, constructor) {
      // 由于 instanceof 要检测的是某对象,需要有一个前置判断条件
      // 基本数据类型直接返回 false
      if (typeof instance !== 'object' || instance == null) return false
      let proto = Object.getPrototypeOf(instance) // 等价于 instance._proto_; 如果该对象没有原型,则返回 null。
      // 当proto == null时,说明已经找到了Object的基类null 退出循环
      while (proto !== null) {
        //  实例的原型等于当前构造函数的原型
        if (proto === constructor.prototype) return true
        // 沿着原型链_proto_一层层向上找
        proto = Object.getPrototypeOf(proto) // 等价于 proto._proto_
      }
      return false
    }

    console.log('test', _instanceof(null, Array)) // false
    console.log('test', _instanceof({}, Array)) // false
    console.log('test', _instanceof([1,2,3,4], Array)) // true
    console.log('test', _instanceof('', Array)) // false

继承有哪几种方式?

js继承的七种方式

继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。 这个下文有提到。

1. 原型链继承 - 重写prototype

实现原理:直接重写构造函数的原型,将构造函数的原型赋值为想要继承的父级构造函数的示例对象。
JavaScript中的每个对象都有一个原型(prototype),原型又是一个对象,同时也有自己的原型,形成了一个原型链。当我们访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript引擎会沿着原型链向上查找,直到找到该属性或方法或到达原型链的顶端(null)。

缺点:

  • 引用类型的属性被所有实例共享,容易造成修改的混乱。
  • 不能给父类构造函数传参
  // 原理:当我们创建 Child 类的实例 child1 并给 names 添加数据时,由于 child1 对象自身没有 names 方法,
  // JavaScript引擎会沿着原型链查找,最终在父类的原型上找到了该方法并执行。
    function Parent () {
      this.names = ['kevin', 'daisy']
    }
    Parent.prototype.getName = function () {
      return this.names
    }

    function Child () {
      this.type = 'Child'
    }
    // 重写Child原型对象; 将子类的原型指向父类的实例
    Child.prototype = new Parent()
    // Child实例上没有constructor,需要手动挂上构造器,指向自己的构造函数
    Child.prototype.constructor = Child;

    let child1 = new Child()
    let child2 = new Child()

    child1.names.push('铁锤妹妹')
    // console.log(child1, 'child1')
    //  所有实例对象上的属性值都发生了变化
    console.log(child1.names, 'child1') // ['kevin', 'daisy', '铁锤妹妹']
    console.log(child2.names, 'child2') // ['kevin', 'daisy', '铁锤妹妹']

2. 构造函数继承 - Parent.call(this)

实现原理:在构造函数内将父级构造函数的this,指向当前构造函数的this,继承父级构造函数的属性和方法。

优点:解决了原型链继承存在的两个缺点。

  • 避免了引用类型的属性被所有实例共享,每个实例对象拥有自己的方法副本(原因我猜想是因为new一个实例对象时候会重新开辟一个新的内存对象)
  • 能给父类构造函数传参

缺点:

  • 方法都在构造函数中定义,每次创建实例都会创建一遍方法;无法共享属性,从而造成对系统资源的浪费
  • 只能继承父级实例上的属性和方法,不能继承父级原型对象 prototype 上的属性和方法。
 function Parent () {
      this.names = ['kevin', 'daisy']
      this.age = 18
    }

    Parent.prototype.getAge = function () {
      return this.age
    }

    function Child (type) {
    // 调用父类的构造函数以继承属性
      Parent.call(this)
      this.type = type
    }

    let child1 = new Child('child')
    let child2 = new Child()

    child1.names.push('Lucy')

    console.log(child1.names) // ["kevin", "daisy", "Lucy"]
    console.log(child2.names) // ["kevin", "daisy"]
    console.log(child1.getAge()) // 会报错 child1.getAge is not a function

在上面的示例中,我们定义了一个父类 Parent ,它有一个 names 属性和一个 getAge 方法。然后我们定义一个子类 Child ,它有一个 type 属性。通过在子类的构造函数中调用父类的构造函数 Parent .call(this),可以将父类的属性继承到子类中。

在这种方式下,父类的属性会被复制到子类的实例中,每个子类实例都有独立的属性副本。因此,对子类实例属性的修改不会影响其他实例。

然而,通过构造函数继承只能实现属性的继承,父类原型上的方法并没有被子类所继承。因此,在上面的示例中,尝试调用 child1.getAge() 会报错,因为 getAge方法不存在于 Child 类或其原型上。

为了解决这个问题,可以使用组合继承(构造函数继承与原型链继承结合)或其他方式来同时实现属性和方法的继承。需要根据具体需求选择合适的继承方式,并理解各种继承方式的特点和限制。

3. 组合继承 – 重写prototype + Parent.call(this)

实现原理:重写构造函数的原型为父级构造函数的实例对象,同时在构造函数内将父级构造函数的 this 指向当前的构造函数。
优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
缺点:Parent 会被调用两次,算是性能方面的缺陷吧,这是不愿看到的。

function Parent3() {
  this.names = ['kevin', 'daisy']
  this.age = 18
}
Parent3.prototype.getAge = function () {
  return this.age
}

function Child3() {
 // 第一次调用 Parent3()
  Parent3.call(this)
  this.type = 'child3'
}
// 第二次调用 Parent3()
Child3.prototype = new Parent3()
//手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3 

var s3 = new Child3()
var s4 = new Child3()
s3.names.push('铁锤妹妹')
console.log(s3.names)  // [ 'kevin', 'daisy', '铁锤妹妹' ]
console.log(s4.names)  // [ 'kevin', 'daisy' ]
console.log('child3 getAge', s3.getAge())  // 正常输出 18

4. 组合继承的优化 - 寄生组合继承

将父类原型对象通过 Object.create() 方法拷贝给到子类的prototype ,这样父类构造函数只执行一次,而且父类属性和方法均能访问。并且子类手动挂上构造器,指向自己的构造函数。
这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承。

function Parent4() {
  this.names = ['kevin', 'daisy']
  this.age = 18
}
Parent4.prototype.getAge = function () {
  return this.age
}
function Child4() {
  Parent4.call(this)
  this.type = 'child4'
}
Child4.prototype = Object.create(Parent4.prototype)
Child4.prototype.constructor = Child4

var group3 = new Child4()
var group4 = new Child4()
group3.names.push('铁锤妹妹')
console.log(group3.names, '寄生组合继承3')   // [ 'kevin', 'daisy', '铁锤妹妹' ]
console.log(group4.names, '寄生组合继承4')   // [ 'kevin', 'daisy' ]
console.log('child4 getAge', group3.getAge())  // 18

5. class类继承

ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像 面向对象编程 的语法而已
虽然原型链继承和构造函数继承都可以实现继承的效果,但它们的语法和实现方式相对较复杂。

  • 原型链继承 需要手动设置 子类的原型 指向 父类的实例。
  • 构造函数继承 需要 子类构造函数 中调用 父类构造函数。

相比之下,class类继承提供了更加简洁、直观的语法,使得继承关系更加易于理解和编写。通过使用 class 关键字定义类extends 关键字指定父类,可以直接在类的内部定义属性和方法,并且可以使用 super 关键字调用父类的构造函数和方法

通过继承,我们可以在不重复编写代码的情况下,实现多个类之间的功能共享和扩展。

  • 类的typeof数据类型就是函数,类本身就指向构造函数
  • ES6 的类,完全可以看作构造函数的另一种写法。
  • 类的所有方法都定义在类的prototype属性上面。
  • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。
  • 使用的时候,也是直接对类使用 new 命令,跟构造函数的用法完全一致。
  • 类的属性和方法,除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)
// 定义一个 Parent 类
 class Parent {
    name: string
    constructor() {
      this.name = 'Parent'
    }
    sayHello() {
      console.log(`Hello, I'm ${this.name}`)
    }
  }
// 定义一个Child类,继承自Parent类
  class Child extends Parent {
    name: string  // 实例对象自身的属性
    constructor() {
    // 形成子类的this对象,把父类的实例属性和方法放到这个this对象上面;
   //相当于Parent.prototype.constructor.call(this)(在子类的this上运行父类的构造函数)
      super() 
      this.name = 'Child'
    }
  }
  
  const child = new Child()
  child.sayHello() // 输出 "Hello, I'm Child"

从设计思想上谈谈继承本身的问题

假如现在有不同品牌的车,每辆车都有drive、music、addOil这三个方法。

class Car{
  constructor(id) {
    this.id = id;
  }
  drive(){
    console.log("wuwuwu!");
  }
  music(){
    console.log("lalala!")
  }
  addOil(){
    console.log("哦哟!")
  }
}
class otherCar extends Car{}

现在可以实现车的功能,并且以此去扩展不同的车。

但是问题来了,新能源汽车也是车,但是它并不需要addOil(加油)。
如果让新能源汽车的类继承Car的话,也是有问题的,俗称"大猩猩和香蕉"的问题。大猩猩手里有香蕉,但是我现在明明只需要香蕉,却拿到了一只大猩猩。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了。

继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。

当然你可能会说,可以再创建一个父类啊,把加油的方法给去掉,但是这也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好。
那如何来解决继承的诸多问题呢?
用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。
顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。

function drive(){
  console.log("wuwuwu!");
}
function music(){
  console.log("lalala!")
}
function addOil(){
  console.log("哦哟!")
}

let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);

代码干净,复用性也很好。这就是面向组合的设计方式。
这个问题让我更加了解了继承本身,出处:从设计思想上谈谈继承本身的问题

JavaScript 创建对象有哪些方式?

1. new Object 构造函数

创建自定义对象可以创建 Object 的一个新实例,再添加属性和方法,如下所示:

let person = new Object();

// 添加属性
person.name = "Lucy";

// 添加方法
person.sayName = function() {
  console.log(this.name)
}

2. 对象字面量

对象字面量创建新对象更为简单直接。如下所示:

let person = {
  name: "Lucy",
  sayName() {
    console.log(this.name)
  }
}

3. Object.create()

Object.create() 方法创建一个新的对象,使用现有的对象作为新创建对象的原型。然后将现有对象返回。

let person = {
  name: "Lucy",
  sayName() {
    console.log(this.name)
  }
}

let person1 = Object.create(person);
console.log(person1.name); // "Lucy"

4. class类 - ES6

类用于创建对象的模版,它建立在原型上。类是“特殊的函数”,类语法有两个组成部分:类表达式 和 类声明
我们用类声明来创建一个对象:

class Person {
  constructor(name) {
    this.name = name;
  }
	// 定义方法
  sayName() {
    console.log(this.name)
  }
}

const person1 = new Person("Lucy");
person1.sayName(); // "Lucy"

注意:函数声明和类声明的一个重要区别是:函数声明会提升,类声明不会
我们用类表达式来创建一个对象,类表达式可以是命名或不命名的,如下用匿名类创建对象:

let Person = class {
  constructor(name) {
    this.name = name;
  }
	// 定义方法
  sayName() {
    console.log(this.name)
  }
}

const person1 = new Person("Lucy");
person1.sayName(); // "Lucy"

类相比原型模式,具备原型模式的优点,代码封装更好。

谈谈你对JS中this的理解。

其实JS中的this是一个非常简单的东西,只需要理解它的执行规则就OK。
call/apply/bind可以显式绑定, 这里就不说了。
主要针对隐式绑定的场景讨论下:

  1. 全局上下文
  2. 直接调用函数
  3. 箭头函数
  4. 对象.方法的形式调用
  5. DOM事件绑定(特殊)
  6. new构造函数绑定
  7. 定时器

1)全局上下文

全局上下文默认this指向window,严格模式下指向undefined。

2)直接调用函数

let obj = {
  a: function() {
    console.log(this);
  }
}
let func = obj.a;
func();

这种情况是直接调用。this相当于全局上下文的情况。

3)箭头函数

箭头函数中的this不能通过apply 、call 和 bind 改变,因为箭头函数中的this指向在定义时已经确认了,之后不会被改变。
箭头函数没有自己的this, 因此也不能绑定。里面的this会指向当前最近的非箭头函数的this,找不到就是window(严格模式是undefined)。
比如:

let obj = {
  a: function() {
    let do = () => {
      console.log(this);
    }
    do();
  }
}
obj.a(); // 找到最近的非箭头函数a,a现在绑定着obj, 因此箭头函数中的this是obj

4)对象.方法的形式调用

还是用 直接调用函数 的例子,我如果这样写:

obj.a()

这就是对象.方法的情况,this指向这个 obj 这个对象,因为 this 永远指向,调用它的对象

5)DOM事件绑定

onclick 和 addEventerListener 中 this 默认指向绑定事件的元素。

document.querySelector('div').onclick = function () {
  console.log(this) //<div></div>
}

6)new构造函数绑定

此时构造函数中的this指向实例对象。

function Person () {
  console.log(this)
  this.name = '铁锤妹妹'
}
var obj = new Person() // 得到一个实例化对象,继承了Person函数的属性
console.log(obj)

打印结果:就是Person

在这里插入图片描述

7)定时器

由setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。这会导致,这些代码中包含的 this 关键字会指向 window (或全局) 对象,这和所期望的this的值是不一样的。

var name = 'windowsName'
    var a = {
      name: '铁锤妹妹',
      func1: function () {
        setTimeout(function () {
          console.log(this.name, 'name')
        }, 100)
      }
    }
    a.func1()
    //因为setTimeout,所以this指向window
    //打印结果:windowsName

优先级:new > call、apply、bind > 对象.方法 > 直接调用。

JS中浅拷贝的手段有哪些?

  • 手动实现

创建一个新的对象遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。

const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}
  • Object.assign()

但是需要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。

let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);  //{ name: 'sss', age: 18 }
  • concat浅拷贝数组
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]
  • slice浅拷贝
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);//[ 1, 2, { val: 1000 } ]

改变了newArr改变了第二个元素的val值,arr也跟着变了。
这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能 解决无限极的对象嵌套问题,实现彻底的拷贝。

  • …展开运算符
let arr = [1, 2, 3];
let newArr = [...arr];  //跟arr.slice()是一样的效果

能不能写一个完整的深拷贝?

1. 简易版及问题

JSON.parse(JSON.stringify());

估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

1) 无法解决循环引用的问题。 举个例子:

const a = {val:2};
a.target = a;

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

2)无法拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map等
3)无法拷贝函数。

因此这个api先pass掉,我们重新写一个深拷贝,简易版如下:

  function deepClone(source: any): any {
    if (!source || typeof source !== 'object') {
      throw new Error('不是对象类型,不能深拷贝')
    }
    const targetObj: any = Array.isArray(source) ? [] : {}
    Object.keys(source).forEach((key: string) => {
      if (source[key] && typeof source[key] === 'object') {
        targetObj[key] = deepClone(source[key])
      } else {
        targetObj[key] = source[key]
      }
    })
    return targetObj
  }

现在,我们以刚刚发现的三个问题为导向,一步步来完善、优化我们的深拷贝代码。

2. 解决循环引用

现在问题如下:

let obj = {val : 100};
obj.target = obj;

deepClone(obj);//报错: RangeError: Maximum call stack size exceeded

这就是循环引用。我们怎么来解决这个问题呢?

首先创建一个Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。

  const isObject = source => (typeof source === 'object' || typeof source === 'function') && source !== null

  const deepClone = (source: any, map = new Map()) => {
    if (map.get(source)) {
      return source
    }
    if (isObject(source)) {
      map.set(source, true)
      const targetObj: any = Array.isArray(source) ? [] : {}
      Object.keys(source).forEach((key: string) => {
        if (source[key] && typeof source[key] === 'object') {
          targetObj[key] = deepClone(source[key], map)
        } else {
          targetObj[key] = source[key]
        }
      })
      return targetObj
    }
  }

现在来试一试:

 const a = { val: 100 }
 a.target = a
 let newA = deepClone(a)
 console.log(newA, 'clone循环引用') //{ val: 100, target: { val: 100, target: [Circular] } }

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是 map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了:

  • 在计算机程序设计中,弱引用与强引用相对, 是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。 --百度百科
  • 弱引用意味着当一个对象作为键被引用时,如果没有其他地方引用这个对象了,垃圾回收机制会自动将该对象从 WeakMap 中删除,释放内存。换句话说,WeakMap 不会阻止对象被垃圾回收,因此它适用于需要临时存储对象的场景。
  • 由于键是弱引用的,所以 WeakMap 没有提供像 Map 那样的迭代方法,也没有 size 属性。此外,WeakMap 的键必须是对象,基本类型的键是不支持的。

说的有一点绕,用大白话解释一下,被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放

怎么解决这个问题?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的。

稍微改造一下即可:

  const deepClone = (source: any, map = new WeakMap()) => {
   // ...
  }

3. 拷贝特殊对象

对于特殊的对象,我们使用以下方式来鉴别:

Object.prototype.toString.call(obj);

梳理一下对于可遍历对象会有什么结果:

["object Map"]
["object Set"]
["object Array"]
["object Object"]
["object Arguments"]

不可遍历的对象:

const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

对于不可遍历的对象,不同的对象有不同的处理。

const handleRegExp = (target: any) => {
    const { source, flags } = target
    return new target.constructor(source, flags)
  }
  const handleNotTraverse = (source: any, tag: any) => {
    let Ctor = source.constructor
    switch (tag) {
      case boolTag:
        return new Object(Boolean.prototype.valueOf.call(source))
      case numberTag:
        return new Object(Number.prototype.valueOf.call(source))
      case stringTag:
        return new Object(String.prototype.valueOf.call(source))
      case symbolTag:
        return new Object(Symbol.prototype.valueOf.call(source))
      case errorTag:
      case dateTag:
        return new Ctor(source)
      case regexpTag:
        return handleRegExp(source)
      default:
        return new Ctor(source)
    }
  }

深拷贝完整代码:

  const getType = obj => Object.prototype.toString.call(obj)
  const isObject = source => (typeof source === 'object' || typeof source === 'function') && source !== null

  const canTraverse = {
    '[object Map]': true,
    '[object Set]': true,
    '[object Array]': true,
    '[object Object]': true,
    '[object Arguments]': true
  }
  const mapTag = '[object Map]'
  const setTag = '[object Set]'
  const boolTag = '[object Boolean]'
  const numberTag = '[object Number]'
  const stringTag = '[object String]'
  const symbolTag = '[object Symbol]'
  const dateTag = '[object Date]'
  const errorTag = '[object Error]'
  const regexpTag = '[object RegExp]'

  const handleRegExp = (target: any) => {
    const { source, flags } = target
    return new target.constructor(source, flags)
  }
  const handleNotTraverse = (source: any, tag: any) => {
    let Ctor = source.constructor
    switch (tag) {
      case boolTag:
        return new Object(Boolean.prototype.valueOf.call(source))
      case numberTag:
        return new Object(Number.prototype.valueOf.call(source))
      case stringTag:
        return new Object(String.prototype.valueOf.call(source))
      case symbolTag:
        return new Object(Symbol.prototype.valueOf.call(source))
      case errorTag:
      case dateTag:
        return new Ctor(source)
      case regexpTag:
        return handleRegExp(source)
      default:
        return new Ctor(source)
    }
  }
  const deepCloneFull = (source: any, map = new WeakMap()) => {
    if (!isObject(source)) {
      return false
    }
    let type = getType(source)
    console.log(type, 'type')
    let targetObj: any
    console.log(canTraverse[type], 'canTraverse[type]')
    if (!canTraverse[type]) {
      // 处理不能遍历的对象
      return handleNotTraverse(source, type)
    } else {
      // 这波操作相当关键,可保证对象的原型不丢失
      let ctor = source.constructor
      targetObj = new ctor()
    }

    if (map.get(source)) {
      return source
    }
    map.set(source, true)

    if (type === setTag) {
      // 处理Set
      source.forEach((item: any) => {
        targetObj.add(deepCloneFull(item, map))
      })
    }
    if (type === mapTag) {
      // 处理Map
      source.forEach((item: any, key: any) => {
        targetObj.set(deepCloneFull(key, map), deepCloneFull(item, map))
      })
    }

    // 处理数组和对象
    Object.keys(source).forEach((key: string) => {
      if (source[key] && typeof source[key] === 'object') {
        targetObj[key] = deepCloneFull(source[key], map)
      } else {
        targetObj[key] = source[key]
      }
    })
    return targetObj
  }

js延迟加载的方式有哪些?

defer 属性在<script> 元素中设置 defer属性,等于告诉浏览器立即下载,但延迟执行,只适用于外部脚本文件。
和async属性、动态创建DOM方式(创建script,插入到DOM中,加载完毕后callBack)
使用setTimeout延迟方法
让JS最后加载(把js外部引入的文件放到页面底部,来让js最后引入,从而加快页面加载速度)

Cookie在客户机上是如何存储的

Cookies就是服务器暂存放在你的电脑里的文本文件,好让服务器用来辨认你的计算机。当你在浏览网站的时候,Web服务器会先送一小小资料放在你的计算机上,Cookies 会帮你在网站上所打的文字或是一些选择都记录下来。当下次你再访问同一个网站,Web服务器会先看看有没有它上次留下的Cookies资料,有的话,就会依据Cookie里的内容来判断使用者,送出特定的网页内容给你。

cookie和session区别

cookie

Cookie是访问某些网站以后在本地存储的一些网站相关的信息
Cookie中一般包括如下主要内容:

  1. key:设置的cookie的key
  2. value:key对应的value
  3. max_age/expire_time:设置cookie的过期时间
  4. domain:该cookie在哪个域名中有效。一般设置子域名,比如cms.example.com。
  5. path:该cookie在哪个路径下有效

例如:我们登录某一个网站时需要输入用户名和密码,如果用户名和密码保存为cookie,则下次我们登录该网站的时候就不需要在输入用户密码了。

session

session是存在服务器的一种用来存放用户数据的类HashTable结构。
浏览器第一次发送请求时,服务器自动生成了—HashTable和—SessionID来唯一标识这个HashTabe,并将其通过响应发送到浏览器。浏览器第二次发送请求会将前一次服务器响应中的SessionID放在请求中一并发送到浏览器上,服务器从请求中提取出sessionID,并和保存的所有sessionID进行对比,找到这个用户对应的HashTable。

例如:我们浏览一个购物网站,用户将部分商品添加到购物车中,许久以前许多网站都是用服务端session存储购物车内容(现在基本都是用数据库了),就用到了session存储这部分信息。

区别

1. 存储位置不同

  • cookie的数据信息存放在本地。
  • session的数据信息存放在服务器上

2. 存储容量大小不同

  • cookie存储的容量较小,一般 <= 4kb
  • session存储容量大小没有限制(但是为了服务器性能考虑,一般不能存放太多数据)。

3. 存储有效期不同

  • cookie可以长期存储,只要不超过设置的过期时间,可以一直存储
  • session在超过一定的操作时间(通常为30分钟)后会失效,但是当关闭浏览器时,为了保护用户信息,会自动调用session.invalidate()方法,该方法会清除掉session中的信息

4. 安全性不同

  • cookie存储在客户端,所以可以分析存放在本地的cookie并进行cookie欺骗,安全性较低。

  • session存储在服务器上,不存在敏感信息泄露的风险,安全性比较高。

    5. 域支持范围不同

  • cookie支持跨域名访问。例如:所有a.com的cookie在a.com下都能用。

  • session不支持跨域访问。例如:www.a.com的session在api.a.com下不能用

    6. 对服务器压力不同

  • cookie保存在客户端,不占用服务器资源

  • session是保存在服务器端,每个用户都会产生一个session,session过多的时候会消耗服务器资源,所以大型网站会有专门的session服务器。

7. 存储的数据类型不同

  • cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据
  • session中能够存储任何类型的数据,包括且不限于string,integer,list,map等

ajax原理

创建一个XHR对象,并发送异步请求,接着监听服务器响应结果,并把它渲染在页面上

什么是JSON??

  • JSON 是一种轻量级的数据交换格式;采用键值对的方式,易于阅读和编写,同时也易于机器解析和生成。
  • 在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
  • 在js中提供了两个函数来实现js数据结构和 JSON 格式的转换处理,JSON.stringify()JSON.parse()
    1) JSON.stringify() ,通过和传入一个符合JSON 格式的数据结构,将其转换为一个JSON 字符串。如果传入的数据结构不符合JSON 格式,那么在序列化时候会对这些值进行对应的特殊处理,使其规范。前端向后端发送数据时,可调用这个函数将数据对象转换为JSON 格式的字符串。
    2)JSON.parse(),将JSON格式的字符串转换为js数据结构,如果传入的字符串不是标准的JSON格式的字符串的话,将会抛出错误。从后端接收到JSON格式的字符串时,可通过这个方法将其解析为js数据结构,以此来进行数据的访问。

git使用过程中,如果你在开发着业务,突然另一个分支有一个bug要改,你怎么办

git stash       //将目前还不想提交的但是已经修改的内容进行保存至暂存区
git stash pop   //将所有暂存区的内容取出来
//过程:当正在dev分支上开发某个项目,这时项目中出现一个bug,需要紧急修复,但是正在开发的内容只是完成一半,还不想提交,这时可以用git stash命令将修改的内容保存至堆栈区,然后顺利切换到hotfix分支进行bug修复,修复完成后,再次切回到dev分支,从堆栈中恢复刚刚保存的内容。

解释一下 JavaScript的同源策略

同源策略是客户端脚本(尤其是Javascript)的安全度量标准,为了防止某个文档或脚本从多个不同源装载
同源策略是一种安全协议,指一段脚本只能读取来自同一来源的窗口和文档的属性
所谓同源就是同域名、同协议、同端口,只有同源的地址才能相互通过ajax方式请求

为什么要有同源限制

我们举例说明:比如一个黑客程序,他利用IFrame把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名,密码登录时,他的页面就可以通过Javascript读取到你的表单中input中的内容,这样用户名,密码就轻松到手了。

图片的预加载和懒加载

预加载:就是在网页全部加载之前,提前加载图片,当用户需要查看时可直接从本地缓存中渲染,以提供给用户更好的体验,减少等待的时间。
懒加载:延迟加载图片或符合某些条件时才加载某些图片。

两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。预加载则会增加服务器前端压力,懒加载对服务器有一定的缓解压力作用。

对虚拟DOM的理解和优缺点

虚拟DOM的实现原理与优缺点

理解

虚拟 DOM 就是为了解决浏览器性能问题而被设计出来的
它是将整个页面抽象为一个 JavaScript 对象树(虚拟DOM),当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异把所记录的差异应用到真正的 dom 树上,视图就更新了。利用DOM diff算法减少不必要的DOM操作和页面重绘,提高性能和用户体验。

虚拟DOM也可以方便实现组件化开发,使得前端代码更具可维护性和可复用性。

优点

1)快速渲染:虚拟 DOM 可以直接操作 JavaScript 对象,无需访问真实浏览器的 DOM。
2)减少不必要的 DOM操作:虚拟 DOM 通过 diff 算法比较前后两个状态的差别,只更新需要改变的部分,减少大量不必要的 DOM 操作,提升性能。
3)跨平台:虚拟 DOM 不依赖具体的浏览器实现,可以在不同的平台上使用,例如服务器渲染,移动端开发等。
4)提高开发效率:虚拟 DOM 可以简化组件的编写和维护,开发效率提高。

缺点

1)需要额外的内存开销:虚拟 DOM 需要创建一颗以 JavaScript 对象表示的虚拟 DOM 树,在一些场景下可能会占用过多的内存。
2)性能瓶颈:虚拟 DOM 在一些复杂场景下可能会导致性能瓶颈,例如大量的列表渲染、频繁的样式变化等。

浏览器渲染机制

详细请点击 浏览器渲染机制

1)处理 HTML 并构建 DOM 树。
2)处理 CSS 构建 CSSOM 树(样式树)。
3)将 DOMCSSOM 合并成一个**渲染树**。
4)根据渲染树来布局,计算每个节点的位置。
5)调用 CPU 绘制,合成图层,显示在屏幕上

第4步和第5步是最耗时的部分,这两步合起来,就是我们通常所说的渲染

浏览器在渲染时的两种现象

重排重绘,重排也叫也叫回流

重排:当 DOM 结构发生变化或者元素样式发生改变时,浏览器需要重新计算元素的几何属性并重新布局,这个过程比较消耗性能。
重绘:指元素的外观样式发生变化(比如改变 color 就叫称为重绘),但是布局没有变,此时浏览器只需要重新绘制元素就可以了,比重排消耗的性能小一些。

重排必定会发生重绘,重绘不一定会引发重排。重排所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列重排

微任务和宏任务

微任务和宏任务是 JavaScript 异步编程中的两个重要概念,它们是为了解决 JavaScript 单线程运行时遇到异步代码所需的执行顺序问题而产生的。

微任务 是指 在当前任务执行完成后立即执行的任务。
宏任务 是指 需要等待事件循环下一轮才能执行的任务。

setTimeout(_ => console.log(4))
new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)
//执行顺序 1,2,3,4

原因是: 执行顺序: 同步任务 》异步任务;异步任务分为微任务和宏任务,微任务优先级大于宏任务。
在这里插入图片描述
nextTick 也是微任务。

CommonJS 和ES6 Module的区别

CommonJS 和 ES6 Module 都是用来组织 JavaScript 代码的模块系统,但它们有以下区别:

  • 导入导出语法不同:CommonJS 中使用 module.exportsexportsrequire组合来实现文件的引入;ES6模块则使用exportimport来实现

  • 动态与静态:CommonJS 对模块依赖的解决是 “动态” 的(只能在运行时,分析出对应的依赖关系)。
    ES6 Module是 “静态” 执行的,主要体现在 import 是静态执行的,不能使用表达式和变量(可以在编译时,就分析出对应的依赖关系,才能做 tree shaking)。

CommonJS 的动态性体现在:
require 的模块路径可以动态指定,支持传入一个表达式,甚至可以通过 if语句 判断是否加载某个模块。
因此,在CommonJS模块被执行前,并没有确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。
ES6 module的静态性体现在:
1)import必须在顶层。(import命令具有提升效果,会提升到整个模块的头部,首先执行)
2)import模块参数只能是字符串,不能是变量; 所以打包工具能够静态分析出依赖关系,并确定知道哪些模块需要被加载、模块的哪些部分被用到。
所以打包时候支持 tree-shaking(死代码检测和排除),而CommonJS不行。
另外 CommonJS模块是运行时加载,ES6模块是编译时输出接口。原因是:
CommonJS 加载的是一个对象(即moudle.exports属性),该对象只有在脚本运行完才会生成。而ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会定义。

  • 同步与异步加载:CommonJS 使用 同步 加载,即在模块内部 require() 一个模块时会等到该模块加载完成后才执行之后的代码,适合在node服务端使用;ES6 Module 使用 异步 加载,在编译阶段确定好依赖关系,然后在运行时根据这个依赖关系动态加载模块,适合在浏览器端使用。
    至于为什么要使用同步和异步加载可以看CommonJS和ES6模块的区别

  • 值拷贝与值的引用: CommonJS 导出是拷贝一份变量出来;一旦这个值被导出,模块内再发生变化不会影响到输出的值;并且改变拷贝的值,不会影响原模板里的值。
    ES6 module中导出的变量是引用类型的,是只读的。对它进行重新赋值会报错,所以不能随意更改值的指向,类似const。(模块中的原始值发生变化,import加载的值也会跟着变)。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError
  • 是否缓存
    CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
    ES6模块是动态引用的,并不会缓存值。(原始值变了,import加载的值也会跟着变。)
  • this关键字: CommonJS 模块的顶层this指向当前模块ES6 模块之中,顶层的this指向undefined

如何理解 JS 的异步?

JS是一门单线程语言,这是因为它运行在浏览器的 渲染主线程 中,而渲染主线程只有一个。
⽽渲染主线程承担着诸多的⼯作,渲染⻚⾯、执⾏ JS 都在其中运⾏。如果使⽤同步的⽅式,就极有可能导致主线程产⽣阻塞,从⽽导致消息队列中的很多其他任务⽆法得到执⾏。
这样⼀来,⼀⽅⾯会导致繁忙的主线程⽩⽩的消耗时间,另⼀⽅⾯导致⻚⾯⽆法及时更新,给⽤户造成卡死现象。
所以浏览器采⽤异步的⽅式来避免。具体做法是当某些任务发⽣时,⽐如计时器、⽹络、事件监听,主线程将任务交给其他线程去处理,⾃身⽴即结束任务的执⾏,转⽽执⾏后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加⼊到消息队列的末尾排队,等待主线程调度执⾏。
在这种异步模式下,浏览器永不阻塞,从⽽最⼤限度的保证了单线程的流畅运⾏。

其实js的异步涉及到了浏览器的线程,这部分想要了解到的更详细关于浏览器的进程、线程请移步查看 浏览器从输入URL到页面渲染加载的过程(浏览器知识体系整理)

js是单线程,为什么可以处理高并发请求?

尽管 JavaScript 是单线程语言,但浏览器Node.js 这样的环境却是多线程的。当高并发请求到达时,浏览器或 Node.js 会将这些请求放在任务队列中进行排队,并且适时地将这些请求分配给可用的线程进行处理,从而实现高并发请求的处理。此外,开发人员还可以使用异步编程方式(如回调函数Promiseasync/await),使得在等待 I/O 操作完成时,JavaScript 可以继续执行其他操作,避免了线程阻塞,从而更好地处理高并发请求。

3 个判断数组的方法,请分别介绍它们之间的区别和优劣

1)Object.prototype.toString.call()

这种方式可以跨文档识别数组,而且比较可靠,但代码略显冗长

2) instanceof

instanceof 使用 instanceof 运算符 可以检查某个值是否为指定类型的实例

在判断一个值是否为数组时,可以使用 Array 构造函数来进行判断。例如:

const arr = [1, 2, 3];
console.log(arr instanceof Array); // 输出 true

3) Array.isArray()

当检测Array实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes。
这个方法不仅可以判断当前窗口或框架中的数组,还可以跨窗口或框架识别数组。例如:

const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // 输出 true

Array.isArray()是ES5新增的方法,用于判断一个值是否为数组类型,但是存在一定兼容性问题;
当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。

综上所述,三种方法并没有明显的优劣之分。选择哪种方式取决于你的具体需求和环境条件。通常情况下建议使用 Array.isArray() 或者 Object.prototype.toString.call() 方法来判断是否为数组类型。

手写实现原生AJAX请求

function ajaxRequest(url, method, data, callback) {
    // 创建 XMLHttpRequest 对象
    const xhr = new XMLHttpRequest()
    // 发送请求
    xhr.open(method, url, true)
    // 注册回调函数,当请求完成时调用
    xhr.onload = function () {
      if (xhr.status === 200) {
        const response = JSON.parse(xhr.responseText)
        callback(response)
      } else {
        callback('请求失败:' + xhr.status)
      }
    }
    xhr.onerror = function () {
      callback('请求错误', null)
    }
    xhr.send(data)
  }
//   使用
  var data = {
    name: 'John',
    age: 25
  }

  function callback(response: any) {
    console.log(response)
  }

  ajaxRequest('http://example.com/api', 'POST', data, callback)

Axios 的本质也是对 XMLHttpRequest 进行封装。

如何使用 Promise 封装 XMLHttpRequest?

使用 Promise 来实现 AJAX 请求,可以利用 XMLHttpRequest 对象并返回一个 Promise 对象。以下是一个使用 Promise 实现 AJAX 的示例:

function ajaxRequest(method, url, data) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    
    xhr.onload = function () {
      if (xhr.status === 200) {
        resolve(xhr.responseText)
      } else {
        reject(xhr.responseText)
      }
    }

    xhr.open(method, url, true); //使用 open() 方法打开与指定 URL 的连接。第三个参数为 true 表示异步请求。

    if (method === 'POST') {
      xhr.setRequestHeader('Content-type', 'application/json');
    }

    xhr.send(JSON.stringify(data));
  });
}

var data = {
  name: 'John',
  age: 25
};

ajaxRequest('POST', 'http://example.com/api', data)
  .then(function(response) {
    console.log(response);
  })
  .catch(function(error) {
    console.error(error);
  });

ajax和axios的区别

Ajax(Asynchronous JavaScript and XML)和 Axios 都是用于向服务器发送请求并获取数据的技术。
Ajax 是一种利用 JavaScript 和 XML 进行数据交换的技术。它可以在不重新加载整个页面的情况下,实现局部更新数据和实时交互效果。使用 Ajax 可以通过异步的方式向服务器发送请求,然后在页面上实时显示数据,提高用户体验。

Axios 是一个基于 Promise 的 HTTP 客户端,可以用于浏览器和 Node.js 环境中,支持请求和响应拦截器、取消请求、自动转换 JSON 数据等功能。Axios 封装了 XMLHttpRequest 和 JSONP 等底层请求机制,提供了更加简洁易用的 API。

以下是 Ajax 和 Axios 的主要区别:

  1. 语法不同:Ajax 主要使用原生的 JavaScript 编写,需要手动创建 XMLHttpRequest 对象,然后通过监听 onreadystatechange 事件来处理异步请求的结果;而 Axios 是基于 Promise 实现的,可以使用链式调用的方式编写请求代码,使得请求代码更加简洁易懂。

  2. 功能不同:Ajax 可以使用原生的 JavaScript 执行跨域请求,但需要后端进行 CORS 配置或者使用 JSONP 等技术;而 Axios 内置了对跨域请求的支持,并且可以自动识别请求数据类型,从而提供更为全面的请求和响应拦截器、取消请求等功能。

  3. 兼容性不同:Ajax 的兼容性较差,在某些旧版本的浏览器中可能无法正常使用;而 Axios 支持多种浏览器和 Node.js 环境,具有更好的兼容性。

  4. 处理方式不同:Ajax 一般使用 回调函数 来处理异步请求的结果,需要手动处理错误和异常情况;而 Axios 使用 Promiseasync/await 来处理异步请求的结果,并且可以设置全局的错误处理和异常处理函数,使得错误处理更加方便。

总的来说,Axios 在语法、功能、兼容性和处理方式上都比 Ajax 更为优秀,因此在实际项目中,建议使用 Axios 来发送异步请求,以提高开发效率和用户体验。

什么是防抖和节流?应用场景、手写代码实现

防抖函数的作用是控制函数在一定时间内的执行次数。简单点说就是通过防抖函数让某个触发事件在 n 秒内只会被执行一次,如果n秒内又触发方法会清除定时器,重新计时执行。
防抖应用场景:

防抖适合多次事件一次响应的情况。
比较典型的有搜索事件,防止用户不断输入过程中,不断请求资源,n秒内只发送1次,用防抖来节约资源。还有按钮点击事件,为了防止用户多次重复提交也会使用防抖函数。最后就是部分的电话号码输入的验证,要等停止输入后才会进行一次验证。

辅助理解:在你坐电梯时,当一直有人进电梯(连续触发),电梯门不会关闭,在一定时间间隔内没有人进入(停止连续触发)才会关闭。

需要注意的是,防抖 debounce 函数返回的函数内部使用 apply 方法调用原函数,保证了传入参数的正常传递。同时,防抖函数内部使用了闭包来保存定时器的引用,确保在多次触发之间可以共享同一个定时器,并且及时清除之前的定时器。
在使用debounce函数后,如果我们不使用 func.apply(context, args)修改this的指向, this就会指向window(ES6下为undefined)

// 防抖函数
export function debounce(func: Function, wait: number, immediate = false) {
  let timer: null | number = null
  return function(this: any, ...args: any[]) {
    const context = this
    const callNow = immediate && !timer
    if (timer) clearTimeout(timer)
    timer = window.setTimeout(() => {
      timer = null
      if (!immediate) func.apply(context, args)
    }, wait)
    if (callNow) func.apply(context, args) //希望让用户点击时,立即提交,等到 n秒后,才可以重新提交。
  }
}

使用示例

import { deBounce } from "@/utils/util"; // 导入函数
const deBounceClick = deBounce(() => { // 注意:防抖函数是个闭包,返回值是一个函数
    // 在这里执行你的提交逻辑
  }, 1000); // 1秒内该按钮没有点击行为则触发提交逻辑,否则,重新计时
<button @click="debounceClick">提交表单</button> <!-- 点击按钮时调用deBounceClick -->

节流函数的作用是在一个单位时间内最多只能触发一次函数执行,如果这个单位时间内多次触发函数,只能有一次生效。
节流应用场景:

节流适合大量事件按时间做平均分配触发。
比较典型的有监听滚动:实现触底加载更多功能。
resize 调整窗口大小事件:调整窗口大小,页面内容随之变动,执行相应的逻辑。
像CSDN写文章这里一样,有自动保存功能,我们一边写,它可以一边保存。
DOM 元素拖拽,固定时间内只执行一次,防止高频率的触发位置变动。
射击游戏的mousedown、keydown时间。

当再次触发事件时, 如果定时器存在,就不执行;等过了设置的等待时间,定时器执行,我们需要在定时器执行时,清空定时器,这样就可以设置下一个定时器了

// 节流(定时器版本)
export function throttle(fn:Function, wait:number) {
  let timer:null|number = null
  return function(this:any, ...args:any[]) {
    const context = this
    if (!timer) {
      timer = window.setTimeout(() => {
        timer = null
        fn.apply(context, args)
      }, wait)
    }
  }
}

防抖和节流的核心就是定时器,通过配合定时器来完成防抖节流函数的手写是很常见的方式。

手把手教你轻松手写防抖和节流
被问了无数次的函数防抖与函数节流,这次你应该学会了吧

项目中如何做优化的?

前端性能优化 24 条建议

ts 跟 js有什么区别,特点,优点和缺点

TypeScript是JavaScript的超集,它向其添加了强类型特征、类、接口等面向对象编程的概念。本质上,TypeScript可以看作是一个为JavaScript提供类型检查和更严格语法的扩展版本,有以下几个区别、特点以及优缺点:
1)语言类型不同
TypeScript 是强类型语言,JavaScript是弱类型语言;强类型语言是指在编译期间进行类型检查,弱类型语言则是在运行期间进行类型检查
2) 类型检查
TypeScript与JavaScript最大的区别在于它允许类型注解和类型检查。使用类型注解可以明确地指定变量、函数的类型,而类型检查可以在代码编写过程中及时发现错误,增强了代码的可维护性和健壮性。

3) 面向对象编程
TypeScript支持接口继承泛型等面向对象编程的概念,这些概念在JavaScript中并不完整或者根本不存在。

4) 更严格的语法
TypeScript对于JavaScript语法进行了扩展,包括对ES6/ES7+的新特性的支持,还有很多自己独有的语言特性,例如元组,枚举,交叉类型,并集类型等。

5) 执行效率
由于TypeScript需要在 编译阶段 进行类型检查和转换,相比较于JavaScript,不可避免地会增加一定的编译时间,同时需要额外的编译步骤。

6) 学习成本
相比于JavaScript,TypeScript拥有更多的语法和概念,需要花费更多的时间来学习。

总的来说,TypeScript相对于JavaScript,优点在于:强类型面向对象编程更严格的语法更好的可维护性;缺点在于:编译时间较长学习成本比较高。另外,TypeScript也并不适用于所有场景,具体应该根据具体的项目需求和团队技术水平进行选择。

ES6 代码转成 ES5 代码的实现思路是什么

说到 ES6 代码转成 ES5 代码,我们肯定会想到 Babel。所以,我们可以参考 Babel 的工作原理。
大致分为三步:

  • 将 ES6 的代码解析生成ES6的AST(抽象语法树);
  • 然后将 ES6 的 AST 转换为 ES5 的AST;
  • 最后才将 ES5 的 AST 转化为具体的 ES5 代码。

Eslint和pretter冲突案例如何解决的?

  • 解决方式一:要么修改 eslintrc,要么修改 prettierrc 配置,让它们配置保持一致;
  • 解决方式二:禁用ESLint中和Prettier配置有冲突的规则;再使用 Prettier 来替代 ESLint 的格式化功能;

git reset 和 git revert 的区别

使用场景

  • git reset :如果想恢复到之前某个提交的版本,且那个版本之后提交的版本我们都不要了,就可以用这种方法。
  • git revert:如果我们想撤销之前的某一版本,但是又想保留该目标版本后面的版本,记录下这整个版本变动流程,就可以用这种方法。

提交历史记录

  • git reset :用于移动分支指针和更改项目提交历史,需要慎用,因为它可能会删除提交。
  • git revert :会反做一个新的commit记录,用于创建新的撤销提交,以保留提交历史。

使用层面:

  • git reset

撤销上一个提交:

git reset --soft HEAD^
// 或者
git reset --hard 目标版本号xxx

撤销多个提交:

git reset HEAD~n  // 将 HEAD 指向要保留的提交之前的提交(n 为要保留的提交数)
git stash    //暂存要丢弃的提交

强制提交到仓库,此时如果用“git push”会报错,因为我们本地库HEAD指向的版本比远程库的要旧:

git push -f

关于 git reset 的补充
git reset 命令用于将 HEAD 移动到指定的提交,并可以选择不同的选项来影响工作区和暂存区的状态。下面是 git reset 命令中几个常用的选项及其区别:

  • soft:之前写的不会改变,之前暂存过的文件还在暂存。
  • mixed:你之前写的不会改变,你之前暂存过的文件不会暂存。
  • hard:文件恢复到所选提交状态,任何更改都会丢失。你已经提交了,然后你又在本地更改了,如果你选hard,那么提交的内容和你提交后又本地修改未提交的内容都会丢失。
  • keep:任何本地更改都将丢失,文件将恢复到所选提交的状态,但本地更改将保持不变。
    你已经提交了,然后你又在本地更改了,如果你选keep,那么提交的内容会丢失,你提交后又本地修改未提交的内容不会丢失。
  • git revert

回滚提交

git revert <commit-hash>
// commit-hash 是要回滚提交的哈希值。

回滚撤销多个版本号的提交

git revert <commit-1> <commit-2> ...
// 将 <commit-1> <commit-2> ... 替换为您要撤销的多个提交的版本号,用空格分隔。

Git 将会创建新的提交以撤销指定的提交。每个提交都会生成一个新的提交记录,将更改还原到之前的状态。

如何针对“多请求接口”进行优化处理?

假如详情页依赖10+个接口,可以通过搭建BFF层,达到服务器内网对内网访问,提升访问速度。

浏览器(外网) =10+接口 =》后端比如JAVA(内网)
浏览器(外网) =1个接口 =》nodejs(BFF内网)=10+接口 =》后端比如JAVA(内网)
  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铁锤妹妹@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值