Skip to content
Updated:

大宝典-Javascript

Table of contents

Open Table of contents

39. 以下哪段代码运行效率更高?(隐藏类)

const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = { a: 1 };
const obj1 = { a: 1 };
const obj2 = { b: 1 };
const obj3 = { c: 1 };
// test
console.time("a");
for (let i = 0; i < 1000000; i++) {
  const obj = {};
  obj["a"] = i;
}
console.timeEnd("a");

console.time("b");
for (let i = 0; i < 1000000; i++) {
  const obj = {};
  obj[i] = i;
}
console.timeEnd("b");

40. 以下哪段代码运行效率更高?(数组- 快速模式/字典模式)

const arr1 = [];
for (let i = 0; i < 1000000; i++) {
  arr1[i] = 1;
}
const arr2 = [];
arr2[1000000 - 1] = 1;
for (let i = 0; i < 1000000; i++) {
  arr1[i] = 1;
}
// test
console.time("a");
const arr1 = [];
for (let i = 0; i < 1000000; i++) {
  arr1[i] = 1;
}
console.timeEnd("a");

console.time("b");
const arr2 = [];
arr2[1000000 - 1] = 1;
for (let i = 0; i < 1000000; i++) {
  arr2[i] = 1;
}
console.timeEnd("b");

[V8 Deep Dives] Understanding Array Internals

41. 如何判断 object 为空

42. 强制类型转换,隐式类型转换

43. == vs ===

1 == "1"; // true
true == 1; // true

1 === "1"; // false
true === 1; // false

补充:当 a = ?以下代码成立

if (a == 1 && a == 2 && a == 3) {
  console.log("hi");
}
const a = {
  i: 1,
  valueOf: function () {
    return this.i++;
  },
};

if (a == 1 && a == 2 && a == 3) {
  console.log("hi");
}

44. JS 的数据类型

45. 如何判断 JS 的数据类型

46. ES 每个版本引入了什么

ECMAScript 是一种用于编写 JS 的标准化脚本语言。下面是每个版本的一些重要特性和区别:

47. let

48. 变量提升 & 函数提升(优先级)

console.log(s);
var s = 2;
function s() {}
console.log(s);

// ƒ s() {}
// 2

49. 如何判断对象相等

较常用 JSON.strinfyg(obj1) === JSON.stringfy(obj2)

lodash/isEqual

50. null vs undefined

undefined

let x;
console.log(x);

const obj = {};
console.log(obj.name);

function fn() {}
console.log(fn());

function add(a, b) {
  return a + b;
}
console.log(add(2));

null

const a = null;
console.log(a);

const obj = { a: 1 };
const proto = obj.__proto__;
console.log(proto.__proto__); // null

51. 用 setTimeout 来实现倒计时,与 setInterval 的区别?

// setTimeout
const countDown = count => {
  setTimeout(() => {
    count--;
    if (count > 0) countDown(count);
    else {
      console.timeEnd("a");
      console.log("ended");
    }
  }, 1000);
};
console.time("a");
countDown(10);
// a: 10099.882080078125 ms
// setInterval
let count = 10;
console.time("a");
let timer = setInterval(() => {
  count--;
  if (count <= 10) {
    console.timeEnd("a");
    clearInterval(timer);
    timer = null;
  }
}, 1000);

注意: setInterval 中当任务执行时间大于任务间隔时间,会导致消费赶不上生产。

JS 事件循环机制 - 宏任务微任务

  1. 同步任务直接执行
  2. 遇到微任务-放到微任务队列(Promise.then/process.nextTick等等)
  3. 遇到宏任务-放到宏任务队列(setTimeout/setInterval等等)
  4. 执行完所有同步任务
  5. 执行微任务队列中的任务
  6. 执行宏任务队列中的任务
console.log(1);
Promise.resolve().then(() => {
  console.log(2);
  setTimeout(() => {
    console.log(3);
  }, 0);
});
setTimeout(() => {
  console.log(4);
  new Promise(resolve => {
    console.log(5);
    resolve();
  }).then(() => {
    console.log(6);
  });
}, 0);
console.log(7);
打印结果? 1,7,2,4,5,6,3

过程分析

// 输出 1 7
// 宏任务列表
const macroTaskQueue = [
  {
    console.log(4);
    new Promise((resolve)=> {
      console.log(5);
      resolve()
    }).then(()=> {
      console.log(6)
    })
  }
]
// 微任务列表
const microTaskQueue = [
  {
    console.log(2);
    setTimeout(()=> {
      console.log(3);
    },0)
  }
]
// 输出 1 7 2
// 宏任务列表
const macroTaskQueue = [
  {
    console.log(4);
    new Promise((resolve)=> {
      console.log(5);
      resolve()
    }).then(()=> {
      console.log(6)
    })
  }
]
// 微任务列表
const microTaskQueue = []
// 输出 1 7 2 4 5
// 宏任务列表
const macroTaskQueue = [
  {
    console.log(3)
  }
]
// 微任务列表
const microTaskQueue = [
  console.log(6)
]
// 输出 1 7 2 4 5 6
// 宏任务列表
const macroTaskQueue = [
  {
    console.log(3)
  }
]
// 微任务列表
const microTaskQueue = []
// 输出 1 7 2 4 5 6 3
// 宏任务列表
const macroTaskQueue = [];
// 微任务列表
const microTaskQueue = [];

53. 事件循环进阶(1)

Promise.resolve()
  .then(() => {
    console.log(0);
    return Promise.resolve(4);
  })
  .then(res => {
    console.log(res);
  });

Promise.resolve()
  .then(() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(4);
  });

过程分析

// 第 1 步
// 输出:
// 微任务:
[
  (() => {
    console.log(0);
    return Promise.resolve(4);
  }).then(res => {
    console.log(res);
  }),
  //
  (() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(4);
  });
];
// 宏任务:
[];
// 第 2 步
// 输出:0
// 处理第一个微任务
(()=> {
  return Promise.resolve().then(()=> {
    return 4
  }).then((res)=> console.log(res) )
})
// 微任务:
[  (() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(5);
  });];
// 宏任务:
[];
// 第 3 步
// 输出:
// 处理第一个微任务
(()=> {
  return Promise.resolve().then(()=> {
    return 4
  }).then(x=> {
    return x;
  }).then((res)=> console.log(res) )
})
// 微任务:
[(() => {
  console.log(1);
})
.then(() => {
  console.log(2);
})
.then(() => {
  console.log(3);
})
.then(() => {
  console.log(5);
});];
// 宏任务
[];
// 第 4 步
// 输出:0
// 微任务:
[(() => {
  console.log(1);
})
.then(() => {
  console.log(2);
})
.then(() => {
  console.log(3);
})
.then(() => {
  console.log(5);
});
//
(()=> {
    return 4
  }).then(x=> {
    return x;
  }).then((res)=> console.log(res) )
];
// 宏任务
[];
// 第 5 步
// 输出:0 1
// 微任务
[
  (() => {
    return 4;
  })
    .then(x => {
      return x;
    })
    .then(res => console.log(res)),
  //
  (() => {
  console.log(2);
})
.then(() => {
  console.log(3);
})
.then(() => {
  console.log(5);
});
];
// 宏任务
[];
// 第 6 步
// 输出:0 1
// 微任务
[
  (() => {
    console.log(2);
  })
    .then(() => {
      console.log(3);
    })
    .then(() => {
      console.log(5);
    }),
  //
  (() => {
    return 4;
  }).then(res => console.log(res)),
];
// 宏任务
[];
// 第 7 步
// 输出:0 1 2
// 微任务
[
  (() => {
    return 4;
  }).then(res => console.log(res)),
  //
  (() => {
    console.log(3);
  }).then(() => {
    console.log(5);
  }),
];
// 宏任务
[];
// 第 8 步
// 输出:0 1 2
// 微任务
[
  (() => {
    console.log(3);
  }).then(() => {
    console.log(5);
  }),
  //
  () => {
    console.log(4);
  },
];
// 宏任务
[];
// 第 9 步
// 输出:0 1 2 3
// 微任务
[
  () => {
    console.log(4);
  },
  () => {
    console.log(5);
  },
];
// 宏任务
[];
// 第 10 步
// 输出:0 1 2 3 4 5
// 微任务
[];
// 宏任务
[];

54. 事件循环进阶(2)

const first = () =>
  new Promise(resolve => {
    console.log(3);
    let p = new Promise(resolve => {
      console.log(7);
      setTimeout(() => {
        console.log(5);
        resolve(6);
        console.log(p);
      }, 0);
      resolve(1);
    });
    resolve(2);
    p.then(arg => {
      console.log(arg);
    });
  });

first().then(arg => {
  console.log(arg);
});

console.log(4);

过程分析

  1. 第一步

输出 2 7 4

const micro = [
  p.then(arg => {
    console.log(arg);
  }),

  first.then(arg => {
    console.log(arg);
  }),
];

const macro = [
  () => {
    console.log(5);
    resolve(6);
    console.log(p);
  },
];
  1. 第二步

执行:

p.then(arg => {
  console.log(arg);
});

输出 3 7 4 1

const micro = [
  first.then(arg => {
    console.log(arg);
  }),
];

const macro = [
  () => {
    console.log(5);
    resolve(6);
    console.log(p);
  },
];
  1. 第三步

执行:

first.then(arg => {
  console.log(arg);
});

输出 3 7 4 1 2

const micro = [];

const macro = [
  () => {
    console.log(5);
    resolve(6);
    console.log(p);
  },
];
  1. 第四步

执行:

() => {
  console.log(5);
  resolve(6);
  console.log(p);
};

输出 3 7 4 1 2 5 Promise(1)

const micro = [];
const macro = [];

55. 事件循环进阶(3)

let a;
let b = new Promise(resolve => {
  console.log(1);
  setTimeout(() => {
    resolve();
  }, 1000);
}).then(() => {
  console.log(2);
});

a = new Promise(async resolve => {
  console.log(a);
  await b;
  console.log(a);
  console.log(3);
  await a;
  resolve(true);
  console.log(4);
});

console.log(5);

涉及 await 的题目,可先简单转换为 Promise ,便于理解

let a;
let b = new Promise(resolve => {
  console.log(1);
  setTimeout(() => {
    resolve();
  }, 1000);
}).then(() => {
  console.log(2);
});

a = new Promise(resolve => {
  console.log(a);
  b.then(() => {
    console.log(a);
    console.log(3);
    a.then(() => {
      resolve(true);
      console.log(4);
    });
  });
});

console.log(5);
  1. 第一步

输出 1 a(undefined) 5

const micro = [
  b.then(() => {
    console.log(a);
    console.log(3);
    a.then(() => {
      resolve(true);
      console.log(4);
    });
  }),
];

const macro = [
  ()=> {
    Promise{b}.resolve();
  },1000);
]
  1. 第二步

因为 b.then 在 setTimeout 中 resolve,所以这里优先执行

  ()=> {
    Promise{b}.resolve();
  },1000);

输出 1 undefined 5 等待一秒

const micro = [
  ()=> {
    console.log(2);
  }).then(() => {
    console.log(a);
    console.log(3);
    a.then(() => {
      resolve(true);
      console.log(4);
    });
  })
];

const macro = []
  1. 第三步

执行:

  ()=> {
    console.log(2);
  }).then(() => {
    console.log(a);
    console.log(3);
    a.then(() => {
      resolve(true);
      console.log(4);
    });
  })

输出 1 undefined 5 等待一秒 2

const micro = [
  () => {
    console.log(a);
    console.log(3);
    a.then(() => {
      resolve(true);
      console.log(4);
    });
  },
];

const macro = [];
  1. 第四步

执行:

() => {
  console.log(a);
  console.log(3);
  a.then(() => {
    resolve(true);
    console.log(4);
  });
};

输出 1 undefined 5 等待一秒 2 Promise{} 3

const micro = [
   a.then(() => {
    resolve(true);
    console.log(4);
  });
]

const macro = []

结束 因为 a.then 需要被 resolve 才会被执行

56. 内存泄漏

应用程序中的内存不再被舒勇但是仍然被占用,导致内存消耗逐渐增加,最终可能导致程序性能下降或崩溃。

通常是因为开发者编写的代码未正确释放不再需要的对象或者数组导致的

特征:: 程序对内存失去控制

案例:

57. 闭包

定义

引用了另一个函数作用域中变量的函数,通常在嵌套函数中发生

作用

可以保留其被定义时的作用域,这意味着

注意

闭包会使函数内部的变量在函数执行后,仍存在于内存中,知道没有任何引用指向闭包。

若不注意管理闭包,可能会导致内存泄漏

案例

const accumulation = function (initial = 0) {
  let result = initial;
  return function () {
    result += 1;
    return result;
  };
};
for (var i = 0; i < 10; ++i) {
  (function (index) {
    setTimeout(function () {
      console.log(index);
    }, 1000);
  })(i);
}

58. 常用的 console

  1. 普通打印 console.log('a');

  2. 按级别打印

    • console.error('a')
    • console.warn('a')
    • console.info('a')
    • console.debug('a')
  3. 占位符

    • console.log('%o a', {a:1})
    • console.log('%s a', 'ss')
    • console.log('%d d', 123)
  4. 打印任何对象,比如 dom 节点 console.dir(document.body)

  5. 打印表格 console.table({a: 1,b: 2})

  6. 计数

    for (let i = 0; i < 10; ++i) {
      console.count("a");
    }
  7. 分组

    console.group("group1");
    console.log("a");
    console.group("group2");
    console.log("b");
    console.groupEnd("group2");
    console.groupEnd("group1");
  8. 计时

    console.time("a");
    const now = Date.now();
    while (Date.now() - now < 1000) {}
    console.timeEnd("a");
  9. 断言 console.assert(1 === 2, 'error')

  10. 调用栈

    function a() {
      console.trace();
    }
    function b() {
      a();
    }
    b();
  11. 内存占用 console.memory

59. 数组去重

  1. 使用 Set

    let array = [1, 2, 3, 2, 4, 1, 5];
    let uniqueArray = [...new Set(array)];
    console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
  2. 使用 filter 和 indexOf

    let array = [1, 2, 3, 2, 4, 1, 5];
    let uniqueArray = array.filter(
      (item, index) => array.indexOf(item) === index
    );
    console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
  3. 使用 reduce

    let array = [1, 2, 3, 2, 4, 1, 5];
    let uniqueArray = array.reduce((acc, item) => {
      if (!acc.includes(item)) {
        acc.push(item);
      }
      return acc;
    }, []);
    console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
  4. 使用 forEach 和 includes

    let array = [1, 2, 3, 2, 4, 1, 5];
    let uniqueArray = [];
    array.forEach(item => {
      if (!uniqueArray.includes(item)) {
        uniqueArray.push(item);
      }
    });
    console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
  5. 使用 Map (适用于对象数组去重)

    let array = [
      { id: 1, name: "Alice" },
      { id: 2, name: "Bob" },
      { id: 1, name: "Alice" },
    ];
    let uniqueArray = Array.from(
      new Map(array.map(item => [item.id, item])).values()
    );
    console.log(uniqueArray); // 输出: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

60. 数组常用方法

const list = [];
// 遍历
for (let i = 0; i < list.length; i++) {}
for (const key in list) {
}
for (const key of list) {
}
list.forEach(i => {});
list.map(i => {}); // 返回构造后的新数组

// 逻辑判断
list.every(i => {}); // 全部 true 即为 true
list.some(i => {}); // 任一 true 即为 true

// filter
list.filter(i => {}); // 返回 过滤后的数组

// 查找
list.indexOf(); // 返回找到的第一个索引,没找到 -1
list.lastIndexOf(); // 返回最后一个找到的位置,没找到 -1
list.includes(); // 如果找到 返回 true
list.find(); // 如果找到,返回目标值,否则 undefined
list.findIndex(); // 如果找到,返回索引,否则 -1

61. reduce

const list = [1, 2, 3, 4, 1];

const sum = list.reduce((prev, cur) => prev + cur);
const maxValue = list.reduce((prev, cur) => Math.max(prev, cur));
const uniqArr = list.reduce((prev, cur) => {
  if (prev.indexOf(cur) === -1) {
    prev.push(cur);
  }
  return prev;
}, []);

const reverseStr = Array.from("hello world").reduce((prev, cur) => {
  return `${cur}${prev}`;
}, "");

62. 如何遍历对象

const obj = { a: 1, b: 2, c: 3 };

for (let key in obj) {
  console.log(key, obj[key]);
}

const keys = Object.keys(obj);
keys.forEach(key => {
  console.log(key, obj[key]);
});

const entries = Object.entries(obj);
entries.forEach(([key, value]) => {
  console.log(key, value);
});

Reflect.ownKeys(obj).forEach(key => {
  console.log(key, obj[key]);
});

63. 创建函数的几种方式

64. 创建对象的几种方式

65. 宿主对象/内置对象/原生对象

  1. 宿主对象(Host Object) 宿主对象是由宿主环境(通常是浏览器或者 nodejs)提供的对象。他们不属于 js 的核心,而是根据运行环境提供的功能而存在

    • 浏览器中的 window,document,XMLHttpRequest
    • nodejs 中的 process,global
  2. 内置对象 js 本身提供的对象,包括全局对象,数学对象,日期对象,正则表达式对象等。例如:

    • 全局对象 Math
    • 日期对象 Date
  3. 原生对象 通过构造函数或者字面量方式创建的对象,例如数组,字符串,函数,对象等。

66. 如何区分数组和对象

  1. 数组(Array)

    • 数组是一种有序的集合,其中每个元素都有一个数字型索引。索引从0开始,依次递增。
    • 数组的元素可以是任意类型的数据,包括数字、字符串、对象、甚至其他数组等。
    • 通常用于存储一组相关的数据,比如一组数字、一组字符串等。
    • 在JavaScript中,可以使用方括号 [] 来创建数组,或者使用 new Array() 构造函数。
    let myArray = [1, 2, 3, 4, 5]; // 创建数组
    let myArray2 = new Array(3); // 使用构造函数创建一个包含3个元素的数组
  2. 对象(Object)

    • 对象是一种键值对的集合,其中每个键(也称为属性)都对应一个值。值可以是任意类型的数据。
    • 对象的键是唯一的,但值可以重复。
    • 对象通常用于表示实体的属性,比如一个人的名字、年龄等。
    • 在 JavaScript 中,对象的字面量形式是使用花括号 {},或者使用 new Object() 构造函数。
    let myObject = { name: "Alice", age: 30, city: "New York" }; // 创建对象
    let myObject2 = new Object(); // 使用构造函数创建一个空对象
    myObject2.name = "Bob"; // 添加属性到对象

数组是一种有序集合,通常用于存储一组相关的数据;而对象是一种键值对的集合,用于表示实体的属性。

67. 什么是类数组(伪数组),如何转为真实的数组

是一种类似数组的对象。具有与数组类似的结构,具有索引和 length 属性。但不具有数组对象上的方法。

常见的类数组:

类数组转为真实数组的方法:

  1. Array.from

    const nodeList = document.querySelectorAll("#test");
    const array = Array.from(nodeList);
  2. Array.prototype.slice.call()

    const nodeList = document.querySelectorAll("#test");
    const array = Array.prototype.slice.call(nodeList);
  3. Spread

    const nodeList = document.querySelectorAll("#test");
    const array = [...nodeList];

68. 什么是作用域链

js 中用于查找变量或函数的一种机制,当代码中某个变量或者函数不在当前作用域(当前执行上下文),js 引擎会像外层作用域查找,直到找到为止,这种嵌套的作用域链形成了一个作用域层级结构。

69. 作用域链如何延长

作用域链的一个重要应用就是闭包。闭包是指函数可以访问其定义时所处的作用域以外的变量。

当一个函数内部定义的函数被外部引用时,闭包就形成了。

这时,内部函数依然可以访问外部函数的作用域,因为他们共享同一个作用域链

闭包

function createCounter() {
  var count = 0;
  return function () {
    count++;
    return count;
  };
}

var counter1 = createCounter();
var counter2 = createCounter();

console.log(counter1()); //1
console.log(counter1()); //2

console.log(counter2()); //1

// 每个 counter 具有自己的作用域链,且都延长了 count 的作用域

70. DOM 的 Attribute 和 Property 的区别

在JavaScript中,DOM(文档对象模型)的Attribute(属性)和Property(属性)之间有一些区别:

  1. Attribute(属性)

    • 属性是 HTML 标签上的声明的静态属性,它们定义了HTML元素的初始值。
    • 通过 getAttribute() 方法可以获取属性的值,通过 setAttribute() 方法可以设置属性的值。
    • 属性值通常在HTML中以字符串形式指定,并且一般与标签的属性相对应,如<input type="text" id="myInput" value="Hello">中的typeidvalue都是属性。
  2. Property(属性)

    • 属性是 DOM 元素的 JavaScript 对象上的动态属性,它们表示了 DOM 元素的当前状态。
    • 通过直接访问 JavaScript 对象的属性来获取和设置属性值。
    • 属性的值通常是对应属性类型的 JavaScript 对象,例如对于 input 元素的value属性,它可以是字符串、数字等类型。`

71. DOM 创建/添加/移除/复制/查找

  1. 创建

    const newEle = document.createElement("div");
    const newTextNode = document.createTextNode("Hello");
    const fragment = document.createDocumentFragment();
  2. 添加

    const newEle = document.createElement("div");
    // 添加子节点
    parentEle.appendChild(newEle);
    // 在参考节点前插入
    parentEle.insertBefore(newEle, referenceEle);
  3. 移除

    parentEle.removeChild(childEle);
  4. 复制

    const cloneEle = originalNode.cloneNode(true);
  5. 查找

    // id
    const ele = document.getElementById("id");
    // 选择器
    const ele = document.querySelector(".class");
    
    // 节点遍历
    const firstChild = parentEle.firstChild;

72. DOM 事件模型

DOM(文档对象模型)事件模型是浏览器用来处理和管理网页上的事件的机制。它定义了事件如何传播、事件处理程序如何附加以及如何在事件中传递数据。DOM 事件模型主要包括三个阶段:捕获阶段、目标阶段和冒泡阶段。

捕获阶段(Capturing Phase)

目标阶段(Target Phase)

冒泡阶段(Bubbling Phase)

事件绑定和处理

在 JavaScript 中,可以通过 addEventListener 方法为 DOM 元素添加事件处理程序。

// 添加一个点击事件处理程序
element.addEventListener(
  "click",
  function (event) {
    console.log("Element clicked!");
  },
  false
); // false 表示在冒泡阶段处理事件

参数说明:

  1. 事件类型(如 ‘click’、‘mouseover’ 等)。
  2. 事件处理程序函数,在事件触发时执行的代码。
  3. useCapture(可选),布尔值,指示事件处理程序是否在捕获阶段触发。默认值为 false(即在冒泡阶段触发)。

事件委托

事件委托是一种将事件处理程序添加到父元素上,而不是直接添加到多个子元素上的技术。利用事件冒泡机制,可以有效地管理大量子元素的事件。

// 为父元素添加事件处理程序
parentElement.addEventListener("click", function (event) {
  if (event.target && event.target.matches("childSelector")) {
    console.log("Child element clicked!");
  }
});

停止事件传播

在某些情况下,可能需要阻止事件传播。可以使用 event.stopPropagation() 方法停止事件在 DOM 树中的传播。

element.addEventListener("click", function (event) {
  event.stopPropagation(); // 停止事件传播
  console.log("Propagation stopped.");
});

默认行为和 preventDefault

有些事件会触发浏览器的默认行为(如表单提交、链接跳转)。可以使用 event.preventDefault() 方法阻止这些默认行为。

linkElement.addEventListener("click", function (event) {
  event.preventDefault(); // 阻止默认的链接跳转行为
  console.log("Default action prevented.");
});

73. 事件三要素

DOM 事件模型中的事件三要素是事件的核心组成部分,它们分别是事件类型、事件目标和事件处理程序。这些要素决定了事件的行为、触发对象以及响应方式。以下是对每个要素的详细解释:

  1. 事件类型(Event Type)

    事件类型指的是事件的具体种类,它决定了事件触发的条件。例如,鼠标点击、键盘输入、页面加载等。常见的事件类型包括:

    • 鼠标事件
      • click:当用户点击某个元素时触发。
      • dblclick:当用户双击某个元素时触发。
      • mouseover:当鼠标指针移到某个元素上方时触发。
      • mouseout:当鼠标指针移出某个元素时触发。
      • mousedown:当用户按下鼠标按钮时触发。
      • mouseup:当用户释放鼠标按钮时触发。
    • 键盘事件
      • keydown:当用户按下键盘按键时触发。
      • keypress:当用户按下并按住键盘按键时触发。
      • keyup:当用户释放键盘按键时触发。
    • 表单事件
      • submit:当表单提交时触发。
      • change:当表单元素的值改变时触发。
      • focus:当元素获得焦点时触发。
      • blur:当元素失去焦点时触发。
    • 窗口事件
      • load:当页面加载完成时触发。
      • resize:当窗口大小变化时触发。
      • scroll:当页面滚动时触发。
  2. 事件目标(Event Target)

    事件目标是事件触发的具体元素。即用户与之交互并触发事件的元素。例如,用户点击按钮时,按钮就是事件的目标。事件对象中可以通过 event.target 属性访问事件目标。

    document
      .getElementById("myButton")
      .addEventListener("click", function (event) {
        console.log("Event target:", event.target); // 输出触发事件的元素
      });
  3. 事件处理程序(Event Handler)

    事件处理程序是当事件触发时执行的函数。它包含处理事件的逻辑。事件处理程序可以通过 addEventListener 方法附加到目标元素上。

    document
      .getElementById("myButton")
      .addEventListener("click", function (event) {
        alert("Button clicked!");
      });

73. 如何绑定事件,解除事件

绑定事件

element.addEventListener("click", () => {});

element.addEventListener("keydown", () => {});

解除事件

element.removeEventListener("click", () => {});

element.removeEventListener("keydown", () => {});

75. 事件冒泡与事件捕获的区别

捕获是向下走,从根节点开始,向下去捕获到目标元素;(可通过 addEventListener 的第三个参数,设为 true 来开启事件捕获)

冒泡是向上走,从目标元素开始,逐级向上冒泡到根节点(是默认的事件传播方式,可通过 stopPropagation 来阻止冒泡)

事件模型

76. 事件委托

事件模型

优点

  1. 性能优势:可减少事件处理程序的数量
  2. 动态元素:适用于动态生成的元素,因为无需为新添加的元素单独绑定事件,而是在祖先元素上继续使用相同的事件处理程序
  3. 代码间接性
  4. 处理多个事件类型

77. JS 动画 vs css3 动画

优点缺点
JS 动画更可控,可使用复杂的逻辑/灵活性高/可使用 requestAnimationFrame 实现更高级的动画性能较差/实现较为复杂
CSS 动画性能好,可利用 GPU 加速/简洁易用/逻辑分离控制力弱,无法精细控制每一帧,动画效果有限/交互能力有限,需要配合 js

适用场景

小结

78. document.write vs innerHtml

  1. 输出位置

    • document.write 将内容直接写入到页面,会覆盖已存在的内容。如果它在页面加载后调用,会覆盖整个页面的内容,因此不建议在文档加载后使用它。
    • innerHTML 是 DOM 元素的属性,可用来设置嚯获取元素的 HTML 内容。
  2. 用法

    • document.write 通常用于在页面加载过程重生成 HTML 内容。不太推荐使用,不宜维护
    • innerHTML 通常用户通过 js 动态更改特定元素的内容。更加灵活
  3. DOM 操作

    • document.write 不是 DOM 操作,仅用于输出文本到页面
    • innerHTML 是 DOM 操作

80. mouseover vs mouseenter

81. 元素拖动

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>元素拖动实例</title>
    <style type="text/css">
      body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
      }

      .draggable {
        width: 100px;
        height: 100px;
        background-color: pink;
        color: black;
        text-align: center;
        line-height: 100px;
        cursor: grab;
        user-select: none;
        position: absolute;
      }
    </style>
  </head>
  <body>
    <div class="draggable" id="draggableEle">拖动我</div>

    <script>
      const draggableEle = document.getElementById("draggableEle");
      let offsetX, offsetY;
      let isDragging = false;
      draggableEle.addEventListener("mousedown", event => {
        isDragging = true;
        offsetX = event.clientX - draggableEle.getBoundingClientRect().left;
        offsetY = event.clientY - draggableEle.getBoundingClientRect().top;
        draggableEle.style.cursor = "grabbing";
      });
      draggableEle.addEventListener("mousemove", event => {
        if (!isDragging) return;
        const newX = event.clientX - offsetX;
        const newY = event.clientY - offsetY;
        draggableEle.style.left = newX + "px";
        draggableEle.style.top = newY + "px";
      });
      draggableEle.addEventListener("mouseup", event => {
        isDragging = false;
        draggableEle.style.cursor = "grab";
      });
    </script>
  </body>
</html>

82. script 的 async vs defer

ES6 的继承和 ES5 的继承的区别

ES6

  1. Classextends 关键字
  2. constructor 构造函数,定义类的初始化逻辑,并通过 super 调用父类的构造函数。
  3. 方法定义:类中的方法不需要使用原型链,而是可以直接定义在类内部
  4. super 用于在子类中调用父类的方法,包括构造函数和普通方法
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log("my name is" + this.name);
  }
}

class Dog extends Animal {
  constructor(name, bread) {
    super(name);
    this.bread = bread;
  }

  speak() {
    console.log("barks" + this.name);
  }
}

const myDog = new Dog("dahuang", "golden");
myDog.speak();

ES5:

84. Promise

js 中处理 异步操作 的一种解决方案,尤其是在处理回调地狱问题上。

三种状态:

模拟实现:

function MyPromise(executor) {
  // 初始化 Promise 的状态和结果
  this._state = "pending";
  this._value = undefined;

  // 回调函数数组,用于存储成功和失败的回调
  this._callback = [];

  // 定义 resolve
  const resolve = value => {
    if (this._state === "pending") {
      this._state = "fulfilled";
      this._value = value;
      this._callback.forEach(item => item.onFulfilled(value));
    }
  };

  // 定义 reject
  const reject = reason => {
    if (this._state === "pending") {
      this._state = "rejected";
      this._value = reason;
      this._callback.forEach(item => item.onRejected(reason));
    }
  };

  // 执行 executor, 传入 resolve 和 reject 作为参数
  try {
    executor(resolve, reject);
  } catch (error) {
    reject(error);
  }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  if ((this._state = "fulfilled")) {
    onFulfilled(this._value);
  } else if (this._state === "rejected") {
    this.onRejected(this._value);
  } else if (this._state === "pending") {
    this._callback.push({
      onFulfilled,
      onRejected,
    });
  }
};

// 实例

const p = new MyPromise((resolve, reject) => {
  setTimeout(() => resolve("成功"), 1000);
});

P.then(
  result => {
    console.log(result);
  },
  error => {
    console.log(error);
  }
);

85. 如何解决异步回调地狱

定义: 嵌套的回调函数中处理多个异步操作,导致代码变得混乱和难以维护的情况

asyncFunc1(function(result1) {​
  asyncFunc2(result1, function(result2) {​
    asyncFunc3(result2, function(result3) {​
      asyncFunc4(result3, function(result4) {​
        // 更多的嵌套回调...​
      });​
    });​
  });​
});

解决方案:

86. 链式调用的实现方式

class Calculator {
  constructor() {
    this.value = 0;
  }

  add(num) {
    this.value += num;
    return this; // 返回自身,以实现链式调用
  }

  minus(num) {
    this.value -= num;
    return this;
  }
  multiply(num) {
    this.value *= num;
    return this;
  }
  divide(num) {
    this.value /= num;
    return this;
  }

  getValue() {
    return this.value;
  }
}

const calculator = new Calculator();

const res = calculator().add(5).minus(1).multiply(3).divide(4).getValue();

87. new 操作符内在逻辑

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

function Person(name) {
  this.name = name;
}
const p1 = myNew(Person, "xxn");
console.log(p1.name);

88. bind vs apply vs call, 以及内在实现

call

apply

bind

89. Ajax 避免浏览器缓存方法

Http 请求时,浏览器会缓存响应数据,以提高性能。

const timestamp = new Date().getTime();
const url = "data.json?timestamp=" + timestamp;

90. eval 的功能和危害

用于执行包含 js 代码的字符串,动态执行字符串内的 js 代码,例如

const x = 1;
const y = 2;
const code = "x + y";
const res = eval(code);

危害:

  1. 安全风险:允许执行来自不受信任来源的代码。如果恶意代码被注入到 eval
  2. 性能问题:运行时解析和执行代码。
  3. 可读性差:入参为字符串的代码,难以分析和调试
  4. 移植性问题:不同 js 引擎对 eval 的实现可能有差异,导致代码在不同环境中出现问题
  5. 限制代码优化:难以进行静态分析和优化

91. 惰性函数

指在第一次调用是执行特定操作,之后将函数重写或修改,以便在后续的调试中直接返回缓存的结果,而不再执行该操作。通常用于性能优化,以避免重复执行开销较大的操作。

function addEvent(element, type, handler) {
  if (element.addEventListener) {
    addEvent = function (element, type, handler) {
      element.addEventListener(type, handler, false);
    };
  } else if (element.attachEvent) {
    addEvent = function (element, type, handler) {
      element.attachEvent("on" + type, handler);
    };
  } else {
    addEvent = function (element, type, handler) {
      element["on" + type] = handler;
    };
  }
  return addEvent(element, type, handler);
}

// 栗子
const btn = document.getElementById("btn");
addEvent(btn, "click", function () {
  console.log("click btn");
});

92. JS 监听对象属性的改变

93. prototype vs __proto__

// 创建一个构造函数
function Person(name) {
  this.name = name;
}
// 在构造函数的 prototype 上定义一个方法
Person.prototype.sayHello = function () {
  console.log(`Hello, my name is ${this.name}`);
};

// 创建一个实例对象
const person1 = new Person("Tom");

// 访问实例对象的属性和方法
console.log(person1.name);
person1.sayHello();

// 查看实例对象的 __proto__ 属性,他指向构造函数的 prototype 对象
console.log(person1.__proto__ === Person.prototype);

94. 如何理解箭头函数没有 this

所谓的 this ,不是箭头函数中没有 this 这个变量,而是箭头i函数不绑定自己的 this,它们会捕获其所爱上下文的 this 值,作为自己的 this。这对于回校函数特别有用,可以避免传统函数中常见的 this 指向问题。例如在对象方法中使用箭头函数可以确保 this 保持一致

95. 上下文与 this 指向

globalThis.a = 100;
function fn() {
  return {
    a: 200,
    m: function () {
      console.log(this.a);
    },
    n: () => {
      console.log(this.a);
    },
    k: function () {
      return function () {
        console.log(this.a);
      };
    },
  };
}

const fn0 = fn();
fn0.m(); // 200 this 指向 {a, m, n}
fn0.n(); // 100 this 指向 globalThis
fn0.k()(); // 100 this 指向 globalThis

const context = { a: 300 };
const fn1 = fn.call(context); // 改变箭头函数 this 指向
fn1.m(); // 200 this 指向 {a, m, n}
fn1.n(); // 300 this 指向 context
fn1.k()(); // 300 this 指向 context

97. 上下文与 this 指向 2

let length = 10;
function fn() {
  return this.length + 1;
}

const obj = {
  length: 5,
  test1: function () {
    return fn();
  },
};

obj.test2 = fn;
console.log(obj.test1()); // window 窗口数量
console.log(fn() === obj.test2()); // false

98. 去除首尾空格

str.trim()

99. Symbol 特性与作用

  1. 唯一性,即使有相同的描述字符串,也不想等
  2. 不可枚举:不会出现在 for...in 循环中
  3. 用作属性名:主要用途是作为对象属性的键,以确保属性的唯一性
  4. 常量:通常用来定义常量,以避免意外的修改值

100. String 的 starstwith vs indexof

101. 字符串转数字

const num1 = parseInt("123");

const num2 = parseFloat("123.456");

const num3 = Number("123");

const num4 = +"123";

102. Promise 和 async/await

关系

103. Array.prototype.sort 在 v8 的实现机制

知识点:默认情况下都会把数组项,转换为 字符串 进行比较

v8 版本查看方式:chrome://version/

104. JS 装箱机制(auto boxing)

const a = 1;
console.log(a.__proto__ === Number.prototype); // true
consolo.log(a instanceof Number); // false
q: 为什么上述代码第二行输出 true,第三行输出 false 首先,基础类型是没有 __proto__ 的,第二行之所以会输出 true,是因为触发了 js 的装箱机制,当一个基础类型尝试访问 __proto__ 的时候,js 会把基础类型临时装箱,理解为 const a = new Number(1) , 所以第二行会输出 true; 而第三行没有触发装箱机制,因此输出 false

详见 JavaScript 的装箱机制(Boxing)和拆箱机制(Unboxing)

105. 函数传值

function test(m) {
  m = { v: 50 };
  console.log(m, "inner");
}

var m = { v: 30 };
test(m); //  {v:50}'inner'
console.log(m.v, "outer"); // 30 'outer'

js 中,对象是按引用传递的。在 test 函数中,m 参数被重新复制 {v:50}, 但这个只是对局部变量 m 的修改,不会影响外部变量

106. 不同类型宏任务的优先级

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button id="test">测试</button>
    <script type="text/javascript">
      function wait(time) {
        const start = Date.now();
        while (Date.now() - start < time) {}
      }

      document.getElementById("test").addEventListener("click", () => {
        console.log("click");
      });

      setTimeout(() => {
        console.log("setTimeout");
      }, 0);

      wait(5000); // 阻塞页面 5s
    </script>
  </body>
</html>

当页面初始化,生成了一个延迟类宏任务。则色页面 5s,而在这 5s 内,点击 test 按钮,新创建了交互类型的宏任务,而交互类型的宏任务优先级要高于延时类型,因此最终页面会先输出 ‘click’,再输出 ‘setTimeout’