一、JavaScript 基础与扩展
1. new 操作符的实现原理
(1)首先创建了一个新的空对象
(2)将对象的__proto__属性指向构造函数的prototype属性
(3)让函数的 this 指向这个对象,执行构造函数的代码
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
function myNew(constructorFn, ...args) {
let newObj = {}
newObj.__proto__ = constructorFn.prototype;
// newObj = Object.create(constructor.prototype);
let result = constructorFn.apply(newObj, args)
return result instanceof Object ? result : newObj
}
function Animal(name) {
this.name = name;
}
let animal = myNew(Animal, 'dog')
console.log(animal);
2. Map 和weakMap 的区别
(1)Map: 本质上是键值对的集合,Object中的键值对中的键只能是字符串,Map 类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构,Map 中的一个键只能出现一次。
(2)WeakMap: 与 Map 结构类似,也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,WeakMap 的键名所指向的对象,不计入垃圾回收机制。
3. JavaScript 有哪些内置对象
NaN、undefined、isNaN()、parseInt() 、Object、Function、Boolean、Symbol、Error、String、Number、Math、Date、Array、Map、Set、WeakMap、WeakSet
4. 对 JSON 的理解
JavaScript Object Notation(JavaScript 对象表示法),是一种基于文本的轻量级的数据交换格式,目前非常多的编程语言都支持JSON。JSON 是一种语法,可以用来序列化对象、数组、字符串等等。在 JavaScript 中提供两个函数来实现 JavaScript 数据结构和 JSON 格式之间转化:
JSON.stringify():将一个 JavaScript 对象或值转换为 JSON 字符串
console.log(JSON.stringify({ x: 5, y: 6 })) // {"x":5,"y":6}
JSON.stringify 的第二个参数:如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
JSON.parse():用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象
const json = '{"result":true, "count":42}';
const obj = JSON.parse(json)
console.log(obj)
5. JavaScript脚本延迟加载的方式有哪些?
延迟加载就是等页面加载完成之后再加载 JavaScript 文件,js 延迟加载有助于提高页面加载速度。
(1)defer 属性: 这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的。
(2)async 属性: 这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的。
(3)动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
(4)使用 setTimeout 延迟: 设置一个定时器来延迟加载js脚本文件
(5)让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
6. JavaScript 类数组(伪数组)对象的定义?
如果一个对象的所有键名都是正整数或零,并且有length属性,那么其被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果。
7. 类数组转换为数组的方法有哪些?
(1)Array.prototype.slice.call(arrayLike)
(2)Array.prototype.concat.apply([], arrayLike)
(3)Array.from(arrayLike)
(4)const arrArgs = [...arguments]
8. 数组有哪些原生方法?
1. 数组和字符串的转换方法:toString()、toLocalString()、join()
2. 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数
3. 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort()
4. 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组
5. 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组
6. 数组插入方法 splice(),影响原数组查找特定项的索引的方法indexOf() 和 lastIndexOf()
7. 数组迭代方法 every()、some()、filter()、map() 和 forEach() 方法
8. 数组归并方法 reduce() 和 reduceRight() 方法
9. 如何遍历类数组?
(1)将数组的方法应用到类数组上
function foo(){
Array.prototype.forEach.call(arguments, a => console.log(a))
}
(2)使用Array.from() 将类数组转化成数组:
function foo(){
const arrArgs = Array.from(arguments)
arrArgs.forEach(a => console.log(a))
}
(3)使用展开运算符将类数组转化成数组
function foo(){
const arrArgs = [...arguments]
arrArgs.forEach(a => console.log(a))
}
10. 什么是 DOM 和 BOM?
- DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口,可以改变网页的内容、结构和样式
- BOM 指的是浏览器对象模型,这个对象主要定义了与浏览器进行交互的方法和接口。BOM的核心是 window,location、navigator、history 称为 window 的子对象,我们可以用他们来移动窗口位置,改变窗口大小,打开新窗口和关闭窗口,获取浏览器品牌版本等。
11. JavaScript中的变量提升
变量提升的表现是,无论在函数中何处声明的变量,都会被放到函数的首部,可以在变量声明前访问到而不会报错。JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
-
在解析阶段:先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境。
-
在执行阶段:按照代码的顺序依次执行。
12. JavaScript为什么要进行变量提升?
(1)提高性能:在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),这是没有必要的。 在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。
(2)容错性更好 :如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
a = 1;var a;console.log(a);
13. 什么是尾调用,使用尾调用有什么好处?
函数的最后一步调用另一个函数:
function f(x){
return g(x)
}
代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中,这样比较占用内存;而使用尾调用的话,因为已经是函数的最后一步,执行尾调用函数时,就可以不必再保留当前执行的上下文,从而节省了内存。
14. 常见的DOM操作有哪些
(1)DOM 节点的获取
getElementById // 按照 id 查询
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询
(2)DOM 节点的创建 :创建一个新节点,并把它添加到指定节点的后面
// 创建子节点
let childern = document.createElement('span')
// 把新创建的元素塞进父节点里去
parent.appendChild(childern)
(3)DOM 节点的删除
// 获取目标元素
let childern = document.getElementById('title')
// 删除目标元素
parent.removeChild(childern)
或者通过子节点数组来完成删除:
// 获取目标元素的父元素
let parent = document.getElementById('parent')
// 获取目标元素var
let childern = parent.childNodes[1]
// 删除目标元素
parent.removeChild(childern)
15. use strict是什么意思?
use strict 是一种严格的运行模式,使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下: 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为,消除代码运行的不安全之处,保证代码运行的安全,提高编译器效率,
在该模式下:
- 禁止使用 with 语句
- 禁止 this 关键字指向全局对象
- 对象不能有重名的属性
16. 如何判断一个对象是否属于某个类?
- 第一种方式,使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
- 第二种方式,通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。
- 第三种方式,使用 Object.prototype.toString() 来进行判断。
17. 强类型语言和弱类型语言的区别
- 强类型语言:要求变量的使用要严格符合定义,所有变量都必须先定义后使用。Java和C++等语言都是强制类型定义的。一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。
- 弱类型语言:JavaScript语言就属于弱类型语言,也就是一种变量类型可以被忽略的语言。在JavaScript中就可以将字符串'12'和整数3进行连接得到字符串'123',在相加的时候会进行强制类型转换。
18. 解释型语言和编译型语言的区别
(1)解释型语言 使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。
特点:
1.解释型语言每次运行都需要将源代码解释称机器码并执行,效率较低
2.只要平台提供相应的解释器,就可以运行源代码,所以可以方便源程序移植
3.JavaScript、Python等属于解释型语言
(2)编译型语言 使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码。在程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。其特点总结如下:
特点:
1.一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高
2.与特定平台相关,一般无法移植到其他平台
3.C、C++等属于编译型语言
19. 数组的遍历方法有哪些
| 方法 | 是否改变原数组 | 特点 |
|---|---|---|
| forEach() | 否 | 数组方法,不改变原数组,没有返回值 |
| map() | 否 | 数组方法,不改变原数组,有返回值,可链式调用 |
| filter() | 否 | 数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用 |
| for...of | 否 | for...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环 |
| every() 和 some() | 否 | 数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false. |
| find() 和 findIndex() | 否 | 数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值 |
| reduce() 和 reduceRight() | 否 | 数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作 |
20. forEach和map方法有什么区别
这方法都是用来遍历数组的,都相当于封装好的单层for循环,两者区别如下:
- forEach()方法没有返回值,返回undefined ;map()方法有返回值,支持链式调用,这意味着可以在对数组执行 map() 方法后附加 reduce() 、 sort() 、 filter() 等操作。
- map() 速度比 forEach() 快
21. 节流和防抖实现及其区别
(1)防抖(debounce):将高频操作优化为只在最后一次执行,如果在 n 秒内再次触发,则会重新计时。例如:输入搜索,在400毫秒只能执行一次,如果又输入了搜索内容,则重新开始计算400毫秒
function debounce(func, delay) {
let timer;
return function() {
let _this = this;
let args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
func.apply(_this, args);
},delay)
}
}
(2)节流(throttle):减少流量,将频繁触发的事件减少,并每隔一段时间执行。例如:点击事件连续点击了n次,但在400毫秒内只能执行一次;使用场景: 滚动条事件 或者 resize 事件。
// 节流
function throttle(func, delay) {
let curTime = new Date();;
return function() {
let context = this;
let args = arguments;
let nowTime = new Date();;
// 判断时间是否超过设置的delay 时间,超过即可执行
if (nowTime - curTime >= delay) {
curTime = new Date();;
return func.apply(context, args);
}
}
}
22. 遍历对象的方法
(1)for in :该遍历方式会得到对象原型链上的属性,可以使用 hasOwnProperty() 过滤掉原型链上的属性(不会返回对象的不可枚举属性)
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(obj[key])
}
}
(2)Object.keys :该方法返回对象自身属性名组成的数组,会自动过滤掉原型链上的属性,可以通过数组的 forEach() 方法来遍历(不会返回对象的不可枚举属性)
Object.keys(obj).forEach((key) => {
console.log(obj[key])
})
(3)Object.getOwnPropertyNames :该方法返回对象自身属性名组成的数组,包括不可枚举的属性,也可以通过数组的 forEach 方法来遍历
Object.getOwnPropertyNames(obj).forEach((key) => {
console.log(obj[key])
})
(4)Object.getOwnPropertySymbols :该方法返回对象自身的 Symbol 属性组成的数组,不包括字符串属性
Object.getOwnPropertySymbols(obj).forEach((key) => {
console.log(obj[key])
})
(5)Reflect.ownKeys:该方法返回对象自身所有属性名组成的数组,包括不可枚举的属性和 Symbol 属性
Reflect.ownKeys(obj).forEach((key) => {
console.log(obj[key])
})
23. 了解 eval() 函数吗?
eval() 是全局对象的一个函数属性,可用于计算某个字符串,并执行其中的 JavaScript 代码。如果参数是一个表达式,eval() 函数将执行表达式;如果参数是Javascript语句,eval()将执行 Javascript 语句
console.log(eval('2 + 2')) // 4
console.log(eval(new String('2 + 2'))) // 2 + 2
console.log(eval('2 + 2') === eval('4')) // true
console.log(eval('2 + 2') === eval(new String('2 + 2'))) // false
MDN文档建议:
24. 主动触发点击事件
点击 button 按钮,触发 input 输入框点击事件,从而实现选择文件
<template>
<input type="file" id="input-upload-file" ref="selectFiles" multiple="multiple">
<el-button class="operation-button-select" @click="handleButtonSelected">选择文件</el-button>
</template>
dispatchEvent(new MouseEvent('click'))
handleButtonSelected() {
// 主动触发点击事件
this.$refs.selectFiles.dispatchEvent(new MouseEvent('click'));
},
25. 点击一个input依次触发的事件
const text = document.getElementById('text');
text.onclick = function (e) {
console.log('onclick')
}
text.onfocus = function (e) {
console.log('onfocus')
}
text.onmousedown = function (e) {
console.log('onmousedown')
}
text.onmouseenter = function (e) {
console.log('onmouseenter')
}
输出结果:
'onmouseenter'
'onmousedown'
'onfocus'
'onclick'
26. setTimeout 的执行原理
执行该语句时,会立即把当前定时器代码推入事件队列,当定时器在事件列表中满足设置的时间值时将传入的函数加入任务队列。如果此时任务队列不为空,则需等待,所以执行定时器内代码的时间可能会大于设置的时间。
27. JS数组方法中哪些会改变原数组,哪些不会?
会改变原数组:push unshift pop shift sort splice reverse
不改变原数组:concat join reduce map forEach filter slice findIndex
28. CommonJs与ESModule的区别
- 两者的模块导入导出语法不同,CommonJs 是通过 module.exports,exports 导出,require 导入;ESModule则是export导出,import导入。
- CommonJs 是运行时加载模块,ESModule 是在静态编译期间就确定模块的依赖。
- ESModule 在编译期间会将所有 import 提升到顶部,CommonJs 不会提升 require。
- CommonJs 导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部;ESModule 是导出的一个引用,内部修改可以同步到外部。
- CommonJs 中顶层的 this 指向这个模块本身,而 ESModule 中顶层 this 指向 undefined。
- CommonJS 加载的是整个模块,将所有的接口全部加载进来,ESModule 可以单独加载其中的某个接口。
29. npm scripts 的原理
npm 允许在package.json文件中,使用scripts字段定义的脚本命令
{
// ...
"scripts": {
"build": "node build.js"
}
}
原理:每当执行npm run,就会自动新建一个 shell,因此只要是 shell(一般是bash)可以运行的命令,都可以写在 npm 脚本中。 npm run 新建的这个 shell,会将当前目录的 node_modules/.bin 子目录加入到 PATH 变量,执行结束之后,再将 PATH 变量恢复。这意味着,当前目录的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。
30. window.open 是干嘛的
window.open 常用来在新的 window 或新的 tab 页打开一个页面或文件,它支持三个参数:
- strUrl:要打开的页面或资源的 url 地址
- strWindowName:窗口的名字,用于后续对该窗口的引用,不是窗口的标题
- strWindowFeatures:窗口的描述参数,如尺寸、位置、是否启用工具栏等
该方法的返回值是新打开的窗口的引用,也就是新窗口的window对象。
31. window.open 的通信问题
使用 window.open 打开新窗口时,原窗口与新窗口之间是可以实现双向通信的。在不跨域的情况下,可以直接访问页面内的任何全局变量、方法或 DOM 元素等。新窗口通过 window.opener 可以找到原窗口的 window;而原窗口可以直接通过 window.open 的返回值访问新窗口,或者通过该窗口的名字找到该窗口。
由于新窗口的加载是异步的,因此不能在调用 window.open 之后立即访问该窗口。可以在窗口的 onload 事件内访问新窗口:
let ref = window.open('/index.html');
ref.onload = function(){
... // 与新窗口通信
}
而在新页面中,可以直接通过window.opener访问原窗口,如:
// 查找原窗口内的p元素
let p = document.querySelectorAll('p',
window.opener.document);
在跨域的情况下,以上的方法会报错,因为会受到浏览器跨域安全策略的限制,此时就需要通过window.postMessage实现页面之间的通信。在原窗口,可以通过对新窗口的引用调用 postMessage:
let ref = window.open('https://www.baidu.com');
ref.postMessage(data, '*');
在新窗口内,同样通过 window.opener 访问原窗口:
window.opener.postMessage(data, '*');
32. 原生JS 实现轮播图
HTML代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,
minimum-scale=1,maximum-scale=1,user-scalable=no" />
<!--引入CSS代码-->
<link rel="stylesheet" type="text/css" href="./css/index.css"/>
<!--引入Js代码-->
<script src="./js/index.js"></script>
<title>Js实现轮播图</title>
</head>
<body>
<div class="lunbo">
<div class="content">
<ul id="item">
<li class="item">
<a href="#"><img src="img/pic1.jpg" ></a>
</li>
<li class="item">
<a href="#"><img src="img/pic2.jpg" ></a>
</li>
<li class="item">
<a href="#"><img src="img/pic3.jpg" ></a>
</li>
<li class="item">
<a href="#"><img src="img/pic4.jpg" ></a>
</li>
<li class="item">
<a href="#"><img src="img/pic5.jpg" ></a>
</li>
</ul>
<div id="btn-left"><</div>
<div id="btn-right">></div>
<ul id="circle">
<li class="circle"></li>
<li class="circle"></li>
<li class="circle"></li>
<li class="circle"></li>
<li class="circle"></li>
</ul>
</div>
</div>
</body>
</html>
CSS代码:
*{
margin: 0;
padding: 0;
}
a{
list-style: none;
}
li{
list-style: none;
}
.lunbo{
width: 100%;
}
.content{
width: 800px;
height: 300px;
margin: 20px auto;
position: relative;
}
#item{
width: 100%;
height: 100%;
}
.item{
position: absolute;
opacity: 0;
transition: all 1s;
}
.item.active{
opacity:1;
}
img{
width: 100%;
}
#btn-left{
width: 30px;
height: 69px;
font-size: 30px;
color: white;
background-color:rgba(0,0,0,0.4);
line-height: 69px;
padding-left:5px;
z-index: 10;/*始终显示在图片的上层*/
position: absolute;
left: 0;
top: 50%;
transform: translateY(-60%);/*使按钮向上偏移居中对齐*/
cursor: pointer;
opacity: 0;/*平时隐藏*/
}
.lunbo:hover #btn-left{
/*鼠标滑入,显示图标*/
opacity: 1;
}
#btn-right{
width: 26px;
height: 69px;
font-size: 30px;
color: white;
background-color:rgba(0,0,0,0.4);
line-height: 69px;
padding-left: 5px;
z-index: 10;
position: absolute;
right: 0;
top: 50%;
cursor: pointer;
opacity: 0;
transform: translateY(-60%);
}
.lunbo:hover #btn-right{
opacity: 1;
}
#circle{
height: 20px;
display: flex;
position: absolute;
bottom: 35px;
right: 25px;
}
.circle{
width: 10px;
height: 10px;
border-radius: 10px;
border: 2px solid white;
background: rgba(0,0,0,0.4);
cursor: pointer;
margin: 5px;
}
.white{
background-color: #FFFFFF;
}
JS代码:
let items=document.getElementsByClassName("item");
let circles=document.getElementsByClassName("circle");
let leftBtn=document.getElementById("btn-left");
let rightBtn=document.getElementById("btn-right");
let content=document.querySelector('.content');
let index=0;
let timer=null;
//清除class
let clearclass=function(){
for(let i=0;i<items.length;i++){
items[i].className="item";
circles[i].className="circle";
circles[i].setAttribute("num",i);
}
}
/*只显示一个class*/
function move(){
clearclass();
items[index].className="item active";
circles[index].className="circle white";
}
//点击右边按钮切换下一张图片
rightBtn.onclick=function(){
if(index<items.length-1){
index++;
}
else{
index=0;
}
move();
}
//点击左边按钮切换上一张图片
leftBtn.onclick=function(){
if(index<items.length){
index--;
}
else{
index=items.length-1;
}
move();
}
//开始定时器,点击右边按钮,实现轮播
timer=setInterval(function(){
rightBtn.onclick();
},1500)
//点击圆点时,跳转到对应图片
for(let i=0;i<circles.length;i++){
circles[i].addEventListener("click",function(){
var point_index=this.getAttribute("num");
index=point_index;
move();
})
}
//鼠标移入清除定时器,并开启一个三秒的定时器,使慢慢转动
content.onmouseover=function(){
clearInterval(timer);
timer=setInterval(function(){
rightBtn.onclick();
},3000)
}
//鼠标移出又开启定时器
content.onmouseleave=function(){
clearInterval(timer);
timer=setInterval(function(){
rightBtn.onclick();
},1500)
}
33. clientX、clientY、offsetLeft、offsetTop....
(1)clientX、clientY:事件发生时,鼠标距离浏览器的可视区域的X、Y轴的位置,不包含滚动条的区域的部分。就算是页面进行了滚动,鼠标的坐标值还是参考可视区域的。
(2)offsetLeft、offsetTop:事件源元素相对于父节点的偏移的像素值。
(3)offsetWidth、offsetHeight:获取的是元素的宽度,包含border,padding,内容宽度,以及滚动条的宽度。
(4)clientWidth和clientHeight:获取的是元素实际的内容宽度,不包含border,padding,内容宽度,以及滚动条的宽度。
(5)scrollTop、scrollHeight:scrollTop 属性可以获取或设置一个元素的内容垂直滚动的像素数,scrollHeight 是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容。scrollHeight = scrollTop + clientHeight
34. 原生JS 实现拖拽功能
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0px;
padding: 0px;
}
#drag {
width: 200px;
height: 200px;
background: red;
cursor: pointer;
position: absolute;
}
</style>
</head>
<body>
<div id="drag"></div>
</body>
<script>
window.onload = function () {
//获取drag元素
let drag = document.getElementById("drag")
//当鼠标按下时
drag.onmousedown = function (e) {
//做到浏览器兼容
e = e || window.event
let diffX = e.clientX - drag.offsetLeft
let diffY = e.clientY - drag.offsetTop
//当拉着box移动时
document.onmousemove = function (e) {
// 浏览器兼容
e = e || window.event
let left = e.clientX - diffX
let top = e.clientY - diffY
if (left < 0) {
left = 0
} else if (left > window.innerWidth - drag.offsetWidth) {
left = window.innerWidth - drag.offsetWidth
}
if (top < 0) {
top = 0
} else if (top > window.innerHeight - drag.offsetHeight) {
top = window.innerHeight - drag.offsetHeight
}
drag.style.left = left + 'px'
drag.style.top = top + 'px'
}
// 当鼠标抬起时
document.onmouseup = function (e) {
this.onmousemove = null
this.onmouseup = null
}
}
}
</script>
</html>
35. JS中Generator函数
(1)Generator的定义和执行:如果说 Promise 是为了解决回调地狱的难题出现的,那么 Generator 就是为了解决异步问题而出现的。
普通函数,如果调用它会立即执行完毕;Generator 函数,它可以暂停,不一定马上把函数体中的所有代码执行完毕,正是因为有这样的特性,它可以用来解决异步问题。
定义一个 Generator 函数,定义的方式和定义一个普通函数是类似的,不同之处在于它在 function 和函数名之间有一个 * 号。
Generator 函数返回是一个迭代器对象,需要通过 xx.next 方法来完成代码执行。在调用 generator 函数时,它只是进行实例化工作,它没有让函数体里面的代码执行,需要通过 next 方法来让它执行。
function* gen() {
console.log(1)
}
// 定义迭代器对象
const iterator = gen()
iterator.next() // 如果不执行这一局代码,1不会被打印
当 next 方法执行时遇到了 yield 就会停止,直到你再次调用 next 方法。
function* gen() {
yield 1
console.log('A')
yield 2
console.log('B')
yield 3
console.log('C')
return 4
}
// 定义迭代器对象
const iterator = gen()
iterator.next() // 执行 gen 函数,打印为空,遇到 yield 1 停止执行
iterator.next() // 继续执行函数,打印 A,遇到 yield 2 停止执行
iterator.next() // 继续执行函数,打印 B,遇到 yield 3 停止执行
iterator.next() // 继续执行函数,打印 C
next 方法调用时,它是有返回值的,它的返回值就是 yield 后面的值或函数的返回值。
// 同步代码
function* gen() {
yield 1
console.log('A')
yield 2
console.log('B')
yield 3
console.log('C')
return 4
}
// 定义迭代器对象
const iterator = gen()
// 异步代码
console.log(iterator.next()) // 打印为空 next返回 {value:1,done:false}
console.log(iterator.next()) // A next返回 {value:2,done:false}
console.log(iterator.next()) // B next返回 {value:3,done:false}
console.log(iterator.next()) // C next返回 {value:4,done:true},如果函数有return值,最后一个next方法,它的value值为return的值 value:4;如果没有。值为 undefined
拓展:其实之所以我们说 Generator 能够把异步变同步,是因为 Generator 函数中我们只需要写同步代码就可以,真正执行异步操作的是迭代器对象。在复杂的业务逻辑中,大量使用迭代器对象来执行异步操作,会使得代码变得很不优雅,于是 ES7 中就推出了 async await 方案来实现异步变同步。在 async await 方案中可以只书写同步代码,真正的异步操作被封装在底层,这样的写法,使得代码变优雅了很多。
(2)Generator中yield在赋值号左边的情况:yield 在等号右边时,它的返回值并不会返回给等号左边的变量,依然会返回给 next 方法。
function* gen(num) {
let r1 = yield 1
console.log('r1', r1);
let r2 = yield 2
console.log('r2', r2);
let r3 = yield 3
console.log('r3', r3);
}
const iterator = gen()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
这是因为 generator 函数在遇到 yield 时就已经暂停执行了,并不会执行到赋值操作,直到在执行完 next 方法之后,才会继续向下执行赋值操作。如果我们想要 r1/r2/r3 有值,我们可以用 next 方法进行传值。就像下面这样:
function* gen(num) {
let r1 = yield 1
console.log('r1', r1);
let r2 = yield 2
console.log('r2', r2);
let r3 = yield 3
console.log('r3', r3);
}
const iterator = gen()
iterator.next() // 第一个 next 方法不用给值,即使给值也不会生效
iterator.next('A')
iterator.next("B")
iterator.next('C')
36. RBAC模型
目前最为普及的权限设计模型是 RBAC 模型, 基于角色的访问控制(Role-Based Access Control)
这是权限最基础也是最核心的模型, 它包括用户 / 角色 / 权限, 其中用户和角色是多对多的关系, 角色和权限也是多对多的关系。
用户是发起操作的主体, 可以是后台管理系统的用户
角色连接了用户和权限, 每个角色可以关联多个权限, 同时一个用户关联多个角色, 那么这个用户就有了多个角色的多个权限。
有人会问了为什么用户不直接关联权限呢?在用户基数小的系统, 比如 20 个人的小系统,管理员可以直接把用户和权限关联,工作量并不大,选择一个用户勾选下需要的权限就完事了。
但是在实际企业系统中,用户基数比较大, 其中很多人的权限都是一样的,就是个普通访问权限,如果管理员给 100 人甚至更多授权, 工作量巨大。
这就引入了 "角色 (Role)" 概念, 一个角色可以与多个用户关联, 管理员只需要把该角色赋予用户, 那么用户就有了该角色下的所有权限, 这样设计提升了效率。
37. 图片的Base64编码
图片的 base64 编码就是可以将一副图片数据编码成一串字符串,使用该字符串代替图像地址。
这样做有什么意义呢?我们知道,我们所看到的网页上的每一个图片,都是需要消耗一个 http 请求下载而来的。不管如何,图片的下载始终都要向服务器发出请求,要是图片的下载不用向服务器发出请求,而可以随着 HTML 的下载同时下载到本地那就太好了,而 base64 正好能解决这个问题。
那么图片的 base64 编码长什么样子呢?举个例子。www.google.com 的首页搜索框右侧的搜索小图标使用的就是base64编码。我们可以看到:
// 在css里的写法
background:url(data:image/gif;base64,R0lGODlhHAAmAKIHAKqqqsvLy0hISObm5vf394uLiwAAAP``///yH5B…EoqQqJKAIBaQOVKHAXr3t7txgBjboSvB8EpLoFZywOAo3LFE5lYs/QW9LT1TRk1V7S2xYJADs=) no-repeat center;
// 在html代码img标签里的写法
<img src="data:image/gif;base64,R0lGODlhHAAmAKIHAKqqqsvLy0hISObm5vf394uLiwAAAP///yH5B…EoqQqJKAIBaQOVKHAXr3t7txgBjboSvB8EpLoFZywOAo3LFE5lYs/QW9LT1TRk1V7S2xYJADs=">
在这里要明确使用 base64 的一个前提,那就是被 base64 编码的图片足够尺寸小。比如:博客园的 Logo 只有 3.27KB,但是如果将其制作转化成 base64 编码,生成的 base64 字符串编码足足有 4406 个。所以我们要明确 base64 的适用场景:如果图片足够小且因为用处的特殊性无法被制作成雪碧图,在整个网站的复用性很高且基本不会被更新
优点:
- 无额外请求
- 对于极小或者极简单图片
- 可像单独图片一样使用,比如背景图片重复使用等
- 没有跨域问题,无需考虑缓存、文件头或者cookies问题
缺点:
- CSS 文件体积的增大
- 页面解析 CSS 生成的 CSSOM 时间增加
38. Map和Object的区别
还有其他的区别:map 只能通过 new 关键字和构造函数创建;object 可以使用字面量、构造函数、Object.crate的形式创建。
39. map与object中key的遍历顺序
40. JSON.stringify深拷贝的缺点
- 如果包含 function,undefined,Symbol,序列化后键值对会消失
- 转换的数据中包含 NaN 序列化后的结果是null
- 转换的数据中包含Date对象,序列化后会变成字符串
- 无法序列化不可枚举属性
- 无法序列化对象的循环引用
41. Object.is的作用以及与===的区别
一个用于比较两个值是否相等的方法, 能够正确处理特殊值,是一个更严格比较值相等的工具。它与 === 操作符的行为有些类似,但在某些情况下有一些微妙的不同之处。
对于原始值,Object.is 的行为类似于 ===
Object.is(NaN, NaN); // true
Object.is(-0, +0); // false
NaN === NaN // false
-0 === +0; // true
对于对象,Object.is 比较的是引用是否相同,而不是对象内容是否相同
const obj1 = { key: 'value' };
const obj2 = { key: 'value' };
const obj3 = obj1;
Object.is(obj1, obj2); // false
Object.is(obj1, obj3); // true
42. LHS 和 RHS 查询
LHS 和 RHS ,是在代码执行阶段 JS 引擎操作变量的两种方式,二者区别就是对变量的查询目的是 变量赋值 还是 查询 。
LHS 可以理解为变量在赋值操作符(=)的左侧,例如 a = 1,当前引擎对变量 a 查找的目的是变量赋值。这种情况下,引擎不关心变量 a 原始值是什么,只管将值 1 赋给 a 变量。
RHS 可以理解为变量在赋值操作符(=)的右侧,例如:console.log(a),其中引擎对变量a的查找目的就是 查询,它需要找到变量 a 对应的实际值是什么,然后才能将它打印出来。
43. includes 比 indexOf 好在哪?
首先,字符串和数组都有 includes 和 indexOf 方法,且功能特性基本一致。两者都是用于判断一个数组中是否存在某个值,其中 includes 返回一个布尔值,indexOf 返回被查找值在数组中第一次出现的位置下标,不存在则返回-1。
includes可以检测NaN,indexOf不能检测NaN,includes内部使用了 Number.isNaN 对 NaN 进行了匹配;由于indexOf内部使用 === 进行判断,所以判断是否存在NaN时始终返回-1。
lastIndexOf 返回被查找值在数组或字符串中最后出现位置的下标
44. (a == 1 && a == 2 && a == 3) 有可能是 true 吗?
方案一:重写toString()或valueOf()
let a = {
i: 1,
toString: function () {
return a.i++;
}
}
console.log(a == 1 && a == 2 && a == 3); // true
方案二:使用Object.defineProperty()
Object.defineProperty()用于定义对象中的属性,接收三个参数:object对象、对象中的属性,属性描述符。属性描述符中get:访问该属性时自动调用。
var _a = 1;
Object.defineProperty(this,'a',{
get:function(){
return _a++
}
})
console.log(a===1 && a===2 && a===3)//true
45. JS为什么是单线程的
这主要和 js 的用途有关,js 主要是实现用户与浏览器的交互,以及操作dom。这决定了它只能是单线程,否则会带来很复杂的同步问题。 举个例子:如果 js 被设计了多线程,这时候有一个线程要修改一个 dom 元素,另一个线程要删除这个 dom 元素,此时浏览器就会一脸茫然,不知所措。所以,为了避免复杂,JavaScript就是单线程。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
46. 深度遍历、广度遍历的区别
- 深度优先不需要记住所有的节点, 所以占用空间小, 而广度优先需要先记录所有的节点占用空间大
- 深度优先有回溯的操作,所以相对而言时间会长一点
- 深度优先采用的是堆栈的形式, 即先进后出
- 广度优先则采用的是队列的形式, 即先进先出
47. forEach如何跳出循环?
forEach是不能通过break或者return来实现跳出循环的,可以利用try catch:
function getItemById(arr, id) {
var item = null;
try {
arr.forEach(function (curItem, i) {
if (curItem.id == id) {
item = curItem;
throw Error();
}
})
} catch (e) {
}
return item;
}
48. JS中如何将页面重定向到另一个页面?
1、window.location.href ="url"
2、window.location.replace("url")
49. JS 中数组在内存中是如何存储的?
- 同种类型数据的数组分配连续的内存空间
- 存在非同种类型数据的数组使用哈希映射分配内存空间
50. nvm作用,nvm自动标记
nvm 是一个用于管理多个 Node.js 版本的工具,它允许你在同一台计算机上安装和切换不同版本的 Node.js,以及方便地切换全局的 Node.js 版本。
nvm 支持自动标记,这是一种自动将当前目录的 Node.js 版本与项目关联的功能。例如,在项目根目录下创建一个名为 .nvmrc 的文件,并在其中指定 Node.js 版本:
14.17.0
当你进入该项目目录时,nvm 会自动切换到 Node.js 版本 14.17.0
51. JS 刷新当前页面的方式
1.window.location.reload()
2.window.location = window.location
3.document.location = document.location
52. String({})和String([])的结果是啥
String({}) -> '[object object]'
String([]) -> ''
53. splice和slice你能说说有啥用和区别吗
54. 为什么 for 循环的性能远远高于 forEach
-
for 循环没有任何额外的函数调用栈和上下文;
-
forEach函数实际上是 array.forEach(function(currentValue, index, arr), thisValue),它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能。
55. a.b.c.d 和 a['b']['c']['d'],哪个性能更好?
a.b.c.d 比 a['b']['c']['d'] 性能高点,因为[ ]里面有可能是字符串,有可能是变量,至少多一次判断,而a.b.c.d是直接取用该字符串当作属性名的。
56. ["1","2","3"].map(parseInt)的输出结果是多少?
const arr = ["1","2","3"].map(parseInt)
console.log(arr) // [1, NaN, NaN]
如何让上述代码返回[1,2,3],使用你能想到的最简单的方案(要求使用[].map())
const arr = ["1","2","3"].map(Number)
console.log(arr) // [1, 2, 3]
56. 使用new调用函数,而这个函数中有return,那它return出来的是什么
结论:当构造函数返回值为对象时,直接返回这个对象;否则返回new创建的对象
测试一:返回值为基本类型
function Thin_User(name, age) {
this.name = name;
this.age = age;
return 'i will keep thin forever';
}
Thin_User.prototype.eatToMuch = function () {
console.log('i eat so much, but i\'m very thin!!!');
}
Thin_User.prototype.isThin = true;
const xiaobao = new Thin_User('zcxiaobao', 18);
console.log(xiaobao.name); // zcxiaobao
console.log(xiaobao.age); // 18
console.log(xiaobao.isThin); // true
xiaobao.eatToMuch(); // i eat so much, but i'm very thin!!!
测试二:返回值为对象
function Thin_User(name, age) {
this.name = name;
this.age = age;
return {
name: name,
age: age * 10,
fat: true
}
}
Thin_User.prototype.eatToMuch = function () {
console.log('i eat so much, but i\'m very thin!!!');
}
Thin_User.prototype.isThin = true;
const xiaobao = new Thin_User('zcxiaobao', 18);
console.log(xiaobao); // {name: 'zcxiaobao', age: 180, fat: true}
xiaobao.eatToMuch(); // Error: xiaobao.eatToMuch is not a function
57. 在 timer = setInterval(() => {}, delay) 中,这个 timer 是什么
此返回值 intervalID 是一个非零数值,用来标识通过 setInterval() 创建的计时器,这个值可以用来作为 clearInterval() 的参数来清除对应的计时器。
58. for ... of 遍历规则
一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可用 for...of 循环遍历它的成员。for...of 循环可以使用的范围包括数组、Set、Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。
59. change 事件与 input 事件的区别
(1)change事件: 当元素更改完成时,才触发 change 事件
-
对于文本输入框,当其
失去焦点时,就会触发 change 事件 -
对于 select、checkbox、radio,会在选项更改后立即触发 change 事件
(2)input事件: 每当用户对输入值进行修改后,就会触发 input 事件
- 只要值改变了,
input事件就会触发,即使那些不涉及键盘行为的值的更改也是如此。例如:使用鼠标粘贴,此时会触发input事件。 - 另一方面,input 事件不会在那些不涉及值更改的键盘输入或其他行为上触发。例如:在输入时按方向键
⇦⇨,此时不会触发 input 事件。
60. 详解 js 函数中的 arguments
简单来说: arguments 是一个对应于传递给函数的参数的类数组对象
arguments 对象是所有(非箭头)函数中都可用的局部变量,你可以使用 arguments 对象在函数中引用函数的参数,此对象包含传递给函数的每个参数。例如,如果一个函数传递了三个参数,你可以以如下方式引用他们:
arguments[0]
arguments[1]
arguments[2]
// 参数也可以被设置
arguments[0] = 'value'
arguments 是一个对象,不是一个 Array ,它类似于 Array,但除了 length 属性和索引元素之外没有任何 Array 属性,它没有 pop 方法,但是它可以被转换为一个真正的 Array。
在严格模式下,剩余参数、默认参数和解构赋值参数的存在不会改变 arguments 对象的行为,但是在非严格模式下就有所不同了:
function func(a) {
arguments[0] = 99; // 更新了arguments[0] 同样更新了a
console.log(a);
}
func(10); // 99
// 并且
function func(a) {
a = 99; // 更新了a 同样更新了arguments[0]
console.log(arguments[0]);
}
func(10); // 99
当非严格模式中的函数没有包含剩余参数、默认参数和解构赋值,那么 arguments 对象中的值会跟踪参数的值(反之亦然)。看下面的代码:
function func(a = 55) {
arguments[0] = 99; // updating arguments[0] does not also update a
console.log(a);
}
func(10); // 10
//
function func(a = 55) {
a = 99; // updating a does not also update arguments[0]
console.log(arguments[0]);
}
func(10); // 10
function func(a = 55) {
console.log(arguments[0]);
}
func(); // undefined
61. JS的parseInt函数
parseInt(string, radix) 解析一个字符串并返回指定基数的十进制整数,radix 是 2-36 之间的整数,表示被解析字符串的基数。
(1)基本用法
1.可以将字符串或小数转化为整数,如果字符串开头有空格(包括\t,\r,\n等),空格会被自动去除
parseInt('123') // 123
parseInt(1.23) // 1
parseInt(' \t\r123') // 123
2.字符串转化为整数的时候,是一个个字符依次转换,如果遇到不能转换的字符,就不再进行下去
parseInt('8a') // 8
parseInt('12***') // 12
parseInt('12.34') // 12
parseInt('0xf00') // 3840 (0xf00代表十六进制)
3.如果一开始为不能转换的字母(x),这种情况直接返回NaN,如果出现了+\-号也会有特殊的处理
parseInt('x') // NaN
parseInt('+') // NaN
parseInt('+12') // 12
parseInt('-') // NaN
parseInt('-12') // -12
4.对空数组不能转换为整数,返回了NaN,而对于可转换的数组也只转换了部分
parseInt([]) // NaN
parseInt([123]) // 123
parseInt([123,456]) // 123
parseInt(['123']) // 123
(2)进制转换
1.该方法还可以接受第二个参数,表示被解析的数(第一个参数)的进制,返回该数对应的10进制值
parseInt(1000,2) // 8
parseInt(1000,6) // 216
parseInt(1000,8) // 512
2.如果第二个参数不是数值,则会自动转换为整数,只有在转换后在2-36之间,才有意义,否则返回NaN
parseInt(10,37) // NaN
parseInt(10,1) // NaN
3.如果第二个参数是0,undefined,null,[],Infinity,NaN等,则直接忽略
parseInt(10,0) // 10
parseInt(10,null) // 10
parseInt(10,undefined) // 10
parseInt(10,[]) // 10
parseInt(10,NaN) // 10
parseInt(10,Infinity) // 10
4.如果第一个参数是数值,会先将这个数值转换为十进制,然后用第二个参数进行进制转换
parseInt(0o16,36) // 40
parseInt(14,36) // 40
(3)科学计数法
该方法会将那些会自动转换为科学计数法的数字视为字符串,读到e的时候就不能解析了
parseInt(1000000000000000000000.5,10) // 1
parseInt('1e+21',10) // 1
parseInt(0.0000005,10) // 5
parseInt('5e-7',10) // 5
62. 谈谈对 javascript面向对象的理解?
63. 数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少
JavaScript 没有真正意义上的数组,所有的数组其实是对象,其“索引”看起来是数字,其实会被转换成字符串,作为属性名(对象的 key)来使用。所以无论是取第 1 个还是取第 10 万个元素,都是用 key 精确查找哈希表的过程,其消耗时间大致相同。
let arr = new Array(100000).fill(null)
console.time('first')
arr[0]
console.timeEnd('first')
console.time('end')
arr[99999]
console.timeEnd('end')
first: 0.010009765625ms
end: 0.003662109375ms
64. 对象键名的转换
- 对象的键名只能是字符串和 Symbol 类型
- 其他类型的键名会被转换成字符串类型
- 对象转字符串默认会调用 toString 方法
// example 1
var a={}, b='123', c=123
a[b]='b'
// c 的键名会被转换成字符串'123',这里会把 b 覆盖掉
a[c]='c'
// 输出 c
console.log(a[b])
// example 2
var a={}, b=Symbol('123'), c=Symbol('123')
// b 是 Symbol 类型,不需要转换
a[b]='b'
// c 是 Symbol 类型,不需要转换。任何一个 Symbol 类型的值都是不相等的,所以不会覆盖掉 b
a[c]='c'
// 输出 b
console.log(a[b])
// example 3
var a={}, b={key:'123'}, c={key:'456'}
// b 不是字符串也不是 Symbol 类型,需要转换成字符串
// 对象类型会调用 toString 方法转换成字符串 [object Object]
a[b]='b'
// c 不是字符串也不是 Symbol 类型,需要转换成字符串
// 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把 b 覆盖掉
a[c]='c'
// 输出 c
console.log(a[b])
65. JS 中的 valueOf() 方法
- JavaScript 中的 valueOf() 方法用于返回指定对象的原始值,若对象没有原始值,则将返回对象本身。通常由JavaScript内部调用,而不是在代码中显式调用。
- 默认情况下,valueOf 方法由 Object 后面的每个对象继承。 每个内置的核心对象都会覆盖此方法以返回适当的值。
- JavaScript 的许多内置对象都重写了该函数,以实现更适合自身的功能需要。因此,不同类型对象的 valueOf() 方法的返回值和返回值类型均可能不同。
Array:返回数组对象本身
Boolean: 返回布尔值
Date:存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC
Function: 返回函数本身
Number: 返回数字值
Object:返回对象本身,这是默认情况
String:返回字符串值
Math 和 Error 对象没有 valueOf 方法
// Array:返回数组对象本身
let array = ["ABC", true, 12, -5];
console.log(array.valueOf() === array); // true
// Date:当前时间距1970年1月1日午夜的毫秒数
// Sun Aug 18 2013 23:11:59 GMT+0800 (中国标准时间)
let date = new Date(2013, 7, 18, 23, 11, 59, 230);
console.log(date.valueOf()); // 1376838719230
// Number:返回数字值
let num = 15.26540; // 15.2654
num.valueOf() // 15.2654
console.log(num.valueOf() === num); // true
// 布尔:返回布尔值true或false
let bool = true;
console.log(bool.valueOf() === bool); // true
// new一个Boolean对象
let newBool = new Boolean(true); // Boolean {true}
newBool.valueOf() // true
// valueOf()返回的是true,两者的值相等
console.log(newBool.valueOf() == newBool); // true
// 但是不全等,两者类型不相等,前者是boolean类型,后者是object类型
console.log(newBool.valueOf() === newBool); // false
// Function:返回函数本身
function foo(){}
console.log( foo.valueOf() === foo ); // true
let foo2 = new Function("x", "y", "return x + y;");
console.log( foo2.valueOf() === foo2); // true
// Object:返回对象本身
let obj = {name: "张三", age: 18};
console.log( obj.valueOf() === obj ); // true
// String:返回字符串值
let str = "http://www.xyz.com";
console.log( str.valueOf() === str ); // true
// new一个字符串对象
// String {"http://www.xyz.com"}
let str2 = new String("http://www.xyz.com");
str2.valueOf() // "http://www.xyz.com"
// 两者的值相等,但不全等,因为类型不同,前者为string类型,后者为object类型
console.log( str2.valueOf() === str2 ); // false
66. for循环和forEach的区别
-
for循环可以使用break跳出循环,但forEach不能
-
for循环可以控制循环起点,forEach只能默认从索引0开始
-
for循环过程中支持修改索引(修改 i),但forEach做不到(底层控制index自增)
-
性能方面:forEach比for循环慢一点
-
使用for循环使代码比在代码中使用forEach更具可读性
67. 使用原生js实现吸顶功能
吸顶功能在前端中很常见,这里以小米为例,在滚动前是这样的
当页面下拉到一定程度就红框标注的部分就会跑到顶部并固定。
- 首先给组件动态绑定样式
-
mounted 中用window.addEventListener()来监听scroll事件
-
在 methods 中实现initHeight()方法
-
销毁事件
68. 如何检查一个对象是否可迭代
只要判断对象属性Symbol.iterator的值是否为function即可
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
69. null 是不是一个对象,如果是,如何判断一个对象是 null
null 是一个原始值,而不是对象,null 表示一个空对象引用,但它本身不是对象。当你检查一个变量是否为 null 时,最好使用严格相等运算符(===)
70. 为什么[] == ![]
按照一定的规则 ![] 转为布尔值false,实际上比较的是[] == false 进行比较之前,会转为Number类型,Number([]) = 0 , Number(false) = 0 所以 [] == ![] 成立 [] == [] false, {} == {} false 都是引用类型 {} == !{} false,转换为 {} == false - > Number({}) = NAN
71. 解释下这段代码
for(var i = 0; i < 3; i++){
setTimeout(function(){
console.log(i)
})
}
// 3
// 3
// 3
异步+块级作用域:因为 setTimeout 函数是异步的,当计时器执行时,for 循环已经结束,变量 i 的值为 3,并且 var 不存在块级作用域,因此三个计时器中的回调函数都会打印 3。
72 . Map转为数组
let map = new Map()
map.set('a', 1)
map.set('b', 2)
// 1. 包含key和value
const arr = Array.from(map) // 得到一个数组,数组的每一项是一个数组(分别为key和value)
// 2. key
const arr2 = Array.from(map.keys())
// 3. value
const arr3 = Array.from(map.values())
map:
array:
73. 遍历JSON对象的方法
for...in、Object.keys() + forEach、Object.entries() + forEach
74. 如何判断JSON是否为空
Object.keys()、JSON.stringify()
75. ES6中数组新增的方法
数组新增的静态方法:Array.from、Array.of
实例对象新增的方法:find、findIndex、entries、keys、values
76. 节流防抖的使用场景
防抖:提交按钮的点击、搜索框搜索输入,只需用户最后一次输入完,再发送请求
节流:滚动事件、窗口大小调整
77. 什么是热点代码
热点代码指的是在程序运行时被频繁执行的代码段
78. 前端错误捕获的方式
try-catch 块:捕获同步代码中的错误
window.onerror 事件:捕获全局的未捕获异常,包括同步和异步错误
unhandledrejection 事件:用于捕获未处理的 Promise 拒绝错误
window.addEventListener('error') 事件:捕获资源加载错误,例如图片加载失败
Vue 框架的错误处理:使用 Vue.config.errorHandler 来处理全局的 Vue 组件错误
79. 纯函数
1.用相同的参数多次调用纯函数,它将始终返回相同的结果;
2.纯函数没有副作用,它不会修改其外部环境,包括全局变量、输入参数对象等
80. Object.create(null) 与{}的区别
Object.create(null) 创建的对象是没有原型方法的 ——Object.create(obj)表示创建一个对象,并给他指定原型对象
{} 其实是new Object(),具有原型方法
81. export和export default的区别
- export default在一个模块中只能有一个,当然也可以没有;export在一个模块中可以有多个。
- export default的对象、变量、函数,可以没有名字;export的必须有名字。
- 两者导出导入语法不同
82. exports 与 module.exports的关系
1.exports 对象是 module 对象的一个属性,在初始时 module.exports 和 exports 指向同一块内存区域
2.模块导出的实际是 module.exports, exports 只是对它的引用。当使用 exports.a = x 的时候,通过引用关系,造成了module.exports.a = x;当使用 exports = x 的时候,造成了 exports 不再指向module.exports,所以仅改变了exports,并没有改变 module.exports,也就并没有对输出起作用。
// a.js
exports.a = 1
module.exports = {
b:2
}
console.log(exports === module.exports) // fasle
// index.js
const module = require('./a.js')
console.log(module) // {b: 2} 只有 module.exports 变量被返回了
83. a 标签默认事件禁掉之后做了什么才实现了跳转
preventDefault()会导致阻止默认行为(即链接跳转),如果需要在阻止了默认行为后,依旧能够实现页面跳转,可以使用location.href
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<a href="www.baidu.com" onclick="myFunction(event)">跳转</a>
<script>
function myFunction(event) {
event.preventDefault();
window.location.href = 'wwwww';
}
</script>
</body>
</html>
二、原型与原型链
1. 对原型、原型链的理解
原型: 函数都有prototype属性,称之为原型,也称为原型对象。原型可以放一些属性和方法,共享给实例对象使用,同时原型可以做继承。
原型链: 每个对象都有_proto 属性,这个属性指向它的原型对象,原型对象也是对象,也有_proto 属性,指向原型对象的原型对象,这样一层一层形成的链式结构称为原型链,最顶层找不到则返回 null。
- prototype属性,它是函数所独有的,它是从一个函数指向一个对象。
- 每个函数都有一个原型对象,该原型对象有一个constructor属性,指向创建对象的函数本身。
2. 原型修改、重写
function Person(name) {
this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
let p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
getName: function() {}
}
let p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // false
可以看到修改原型的时候 p 的构造函数不是指向 Person 了,因为给 Person 的原型对象直接用对象赋值时,它的构造函数指向了根构造函数 Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:
Person.prototype = {
getName: function() {}
}
let p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
3. 原型链指向
p.__proto__ // Person.prototype
Person.prototype.__proto__ // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor // Person
具体解释可以观察以下的图片:
4. 原型链的终点是什么?如何打印出原型链的终点?
由于 Object 是构造函数,原型链终点是 Object.prototype.proto ,而 Object.prototype.proto=== null // true ,所以,原型链的终点是 null 。原型链上的所有原型都是对象,所有的对象最终都是由 Object 构造的,而 Object.prototype 的下一级是 Object.prototype.proto 。
5. 如何获得对象非原型链上的属性?
使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:
function iterate(obj){
let res=[];
for(var key in obj){
if(obj.hasOwnProperty(key))
res.push(key+': '+obj[key]);
}
return res;
}
6. 关于数组的原型链
let arr = [344, 45, 34, 23]; //一个普通数组
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
console.log(Array.prototype.hasOwnProperty('push')); // true
console.log(Array.prototype.hasOwnProperty('splice')); // true
7. Object的_proto_指向什么
Object 是构造函数,所有的函数都是通过 new Function 创建了,因此 Object 相当于 Function 的实例,即 Object.proto --> Function.prototype。
8. Function的_proto_指向什么
Function 函数不通过任何东西创建,JS引擎启动时,添加到内存中。 Function.proto --> Function.prototype
三、闭包与作用域
1. 什么是闭包
闭包就是指有权访问另一个函数作用域中的变量的函数,是一种保存和保护内部私有变量的机制。也可以认为闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。
2. 闭包的作用(优点)
- 使我们在函数外部能够访问到函数内部的变量
- 变量私有化,防止全局污染
3. 闭包带来的问题(缺点)
不建议过多的使用闭包,因为使用不被释放的上下文,是占用栈内存空间的,过多的使用会导致导致内存泄漏。解决闭包带来的内存泄漏问题的方法是:使用完闭包函数后手动释放。
4. 闭包的使用场景
- return 一个函数
- 循环赋值
- 节流防抖
- 函数柯里化
5. 经典面试题
循环中使用闭包解决 var 定义函数的问题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。解决办法有两种:
- 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
- 第二种就是使用
let定义i了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
四、作用域/作用域链
1. 谈谈你对作用域的理解
简单来说作用域就是变量的有效范围,在一定的空间里可以对变量数据进行读写操作,这个空间就是变量的作用域。
(1)全局作用域
-
直接写在script标签的JS代码,都在全局作用域,在全局作用域下声明的变量叫做全局变量。
-
全局变量在全局的任何位置下都可以使用,全局作用域中无法访问到局部作用域中的变量。
-
全局作用域在页面打开的时候创建,在页面关闭时销毁。
-
所有 window 对象的属性拥有全局作用域
var和function命令声明的全局变量和函数是window对象的属性和方法
let命令、const命令、class命令声明的全局变量,不属于window对象的属性
(2)函数作用域(局部作用域)
- 调用函数时会创建函数作用域,函数执行完毕以后,作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的。
- 在函数作用域中可以访问全局变量,在函数的外面无法访问函数内的变量。
- 当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有就向上一作用域中寻找,直到找到全局作用域,如果全局作用域中仍然没有找到,则会报错。
(3)块级作用域
- 任何一对花括号{}中的语句集都属于一个块,在块中使用let和const声明的变量,外部是访问不到的,这种作用域的规则就叫块级作用域。
- 通过var声明的变量或者非严格模式下创建的函数声明没有块级作用域。
(4)词法作用域
- 词法作用域是静态的作用域,无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
- 换句话说,词法作用域就是你在写代码的时候就已经决定了变量的作用域。
2. 什么是作用域链
当在js中使用一个变量的时候,首先js引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域,这样的变量作用域访问的链式结构, 被称之为作用域链。
(1)作用域链的作用
可以访问到外层环境的变量和函数。
(2)作用域的应用场景
作用域的一个常见运用场景就是 模块化。
由于 javascript 并未支持模块化导致了很多问题,比如全局作用域污染和变量名冲突。在正式的模块化方案出台之前,开发者为了解决这类问题,想到了使用函数作用域来创建模块的方案。
(3)说说Js中的预解析?
JS 引擎在运行一份代码的时候,会按照下面的步骤进行工作:
1.把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值
2.把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用
3.先提升 function,在提升 var
(4)变量提升与函数提升的区别?
变量提升
在 JavaScript 代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端,函数内声明的变量只会提升至该函数作用域最顶层,当函数内部定义的一个变量与外部相同时,那么函数体内的这个变量就会被上升到最顶端。
函数提升
函数提升只会提升函数声明式写法,函数表达式的写法不存在函数提升。函数提升的优先级大于变量提升的优先级,即函数提升在变量提升之上
(5)如何延长作用域链?
- try - catch 语句的 catch 块:会创建一个新的变量对象,包含的是被抛出的错误对象的声明。
- with 语句:会将指定的对象添加到作用域链中