文章目录
- 前言
- 数据类型:
- JavaScript基础
- 1. let、const 以及 var 的区别是什么?
- 2. 在 JS 中什么是变量提升?什么是暂时性死区?
- 3. 为什么0.1+0.2 ! == 0.3,如何处理
- 4. `for of` , `for in` 和 `forEach`,`map` 的区别
- 5. this 指向问题
- 6. 谈谈你对 JS 执行上下文栈和作用域链的理解。
- 7. 什么是闭包?闭包的作用是什么?闭包的使用场景?闭包的性能问题?
- 8. call、apply 和 bind 方法的用法、区别和使用场景
- 9. 谈谈你对原型的理解?什么是原型链?原型链解决的是什么问题?prototype 和 __proto__ 区别是什么?
- 10. 聊一聊 DOM 事件响应机制,事件流,事件冒泡和事件捕获是什么?现代浏览器默认的响应机制是什么?
- 11. 谈谈你对严格模式 "use strict" 的理解
- 12. 常见的DOM操作有哪些
- 13. 数组有哪些原生方法
- 14. setTimeout 倒计时为什么会出现误差?
- 15. 常用的正则表达式有哪些
- 16. 对JSON的理解
- 17. 为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?
- JavaScript手写题 (难)
- 1. 对AJAX的理解,实现一个AJAX请求
- 2. 使用Promise封装AJAX请求
- 3. 什么是深拷贝?深拷贝和浅拷贝有什么区别?
- 4. instanceof 操作符的实现原理及实现
- 5. call、apply 和 bind 方法的用法、区别和使用场景
- 6. 手写 实现 call、apply 和 bind 方法
- 7. new 的原理是什么?new 的模拟实现?通过new 的方式创建对象和通过字面量创建有什么区别?
- 8. 详解 JS 继承,从原理入手到 五种 JS 实现继承方式
- 9. 手写 Object.create
- 10. 取数组的最大值(ES5、ES6), 详解Math.max() 和 reduce() API
- ES6+部分
- Axios和Fetch
- 浏览器
- 其他
- 参考
前言
再难苟且的生活还要继续,饭碗还是要继续找的🤖
整理了一周,由浅入深,每(zhěng)天(lǐ)整(bù)理( yì)10条( qiú)左(diǎn)右(zàn), 希望对正在找工作的小伙伴有点帮助,文中如有表述不对,还请指出。
数据类型:
1. JavaScript 的数据类型有那些?
其实在今年的早些时候,null 还不属于原始类型。
早些时候的数据类型划分:
原始值( primitive values )
除 Object
以外的所有类型都是不可变的(值本身无法被改变)。例如,与 C 语言不同,JavaScript 中字符串是不可变的(译注:如,JavaScript
中对字符串的操作一定返回了一个新字符串,原始字符串并没有被改变)。我们称这些类型的值为 “原始值”
。
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
2. typeof 能判断哪些类型?
typeof 能判断所有的原始类型
我们可以看出typeof能正确的判断出6种原始类型。不能用来判断null或者引用数据类型object(对象、数组和函数)。
对于原始数据类型,我们可以使用typeof()函数来判断他的数据类型:
console.log(typeof "");
console.log(typeof 1);
console.log(typeof true);
console.log(typeof null);
console.log(typeof undefined);
console.log(typeof []);
console.log(typeof function(){});
console.log(typeof {});
看看控制台输出什么
可以看到,typeof对于基本数据类型判断是没有问题的,但是遇到引用数据类型(如:Array)是不起作用的。
typeof 能判断是否是函数
//typeof 能判断函数
typeof console.log(1) // function
typeof function fn () {} // function
typeof 能判断出是否是引用类型(不可细分)
//typeof 判断引用类型
const a = null typeof a //object
const a = { a: 100 } typeof a // object
const a = ['a'] typeof a // object
3. 数据类型检测的方式有哪些
该题内容已详细记录在这篇文章中: 一文搞定JavaScript的数据类型检测_前端圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/117467989
4. 判断数组的方式有哪些
- 通过Object.prototype.toString.call()做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
- constructor
obj.constructor === Array
- 通过ES6的Array.isArray()做判断
Array.isArrray(obj);
- 通过instanceof做判断
obj instanceof Array
- 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)
测试一下上面总结的五种方法:
var obj = {};
var arr = [1, 2, 3];
// instanceof
var instanceofObj = obj instanceof Array;
console.log(instanceofObj);
var instanceofArr = arr instanceof Array;
console.log(instanceofArr);
// Array.isArray()
var objout2 = Array.isArray(obj);
console.log(objout2);
var arrout2 = Array.isArray(arr);
console.log(arrout2)
// Array.prototype.isPrototypeOf()
var objout3 = Array.prototype.isPrototypeOf(obj);
console.log(objout3);
var arrout3 = Array.prototype.isPrototypeOf(arr);
console.log(arrout3);
// constructor
var objout4 = (obj.constructor === Array)
console.log(objout4);
var arrout4 = (arr.constructor === Array)
console.log(arrout4)
// obj.prototype.toString.call()
var objout5 = Object.prototype.toString.call(obj);
console.log(objout5);
var arrout5 = Object.prototype.toString.call(arr);
console.log(arrout5);
输出结果:
5. null 和 undefined 的区别, typeof null 的结果是什么,为什么?
undefined
对声明但未赋值的变量返回类型为 undefined 表示值未定义。
let hd;
console.log(typeof hd);
对未声明的变量使用会报错,但判断类型将显示 undefined。
未赋值与未定义的变量值都为 undefined ,建议声明变量设置初始值,这样就可以区分出变量状态了。
函数参数或无返回值是为undefined
function hd(web) {
console.log(web); //undefined
return web;
}
console.log(hd()); //undefined
null
null 用于定义一个空对象,即如果变量要用来保存引用类型,可以在初始化时将其设置为null。
var hd = null;
console.log(typeof hd);
typeof null 的结果是什么,为什么?
我们知道typeof null的结果是object,typeof undefined的结果是undefined。
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 “object”。
曾有一个 ECMAScript 的修复提案(通过选择性加入的方式),但被拒绝了。该提案会导致 typeof null === ‘null’。其实这是js设计的一个败笔,早期准备更改null的类型为null,由于当时已经有大量网站使用了null,如果更改,将导致很多网站的逻辑出现漏洞问题,就没有更改过来,于是一直遗留到现在。
6. == 和 === 的区别,js隐式类型转换步骤
先看MDN对两个运算符的解释:
双等号是相等比较,三等号是严格相等比较。
简而言之,在比较两个值,双等号将执行类型转换; 三等号将进行相同的比较,而不进行类型转换 (如果类型不同, 只是总会返回 false );
javascript双等号引起类型转换,下面详解:
隐式类型转换步骤
一、首先看双等号前后有没有NaN,如果存在NaN,一律返回false。
二、再看双等号前后有没有布尔,有布尔就将布尔转换为数字。(false是0,true是1)
三、接着看双等号前后有没有字符串, 有三种情况:
1、另一个是对象,对象使用toString()或者valueOf()进行转换;
2、另一个是数字,字符串转数字;
3、另一个也是字符串,直接比较;
4、其他返回false
四、如果是数字,对方是对象,对象取valueOf()或者toString()进行比较, 其他一律返回false
五、null, undefined不会进行类型转换, 但它们俩相等
7. js数据类型转换
在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值(调用
Boolean()
方法) - 转换为数字(调用
Number()
、parseInt()
和parseFloat()
方法) - 转换为字符串(调用
.toString()
或者String()
方法)
null
和underfined
没有.toString()
方法
8. [] == ![] 结果是什么?
我们来分析一下: [] == ![] 是true还是false?
- 首先,我们需要知道 ! 优先级是高于 ==
[]
引用类型转换成布尔值都是true,因此![]
的是false- 其中一方是 boolean,则将 boolean 转为 number 再进行判断,false转换成 number,对应的值是 0
- 有一方是 number,那么将object也转换成Number,空数组转换成数字,对应的值是0.(空数组转换成数字,对应的值是0,如果数组中只有一个数字,那么转成number就是这个数字,其它情况,均为NaN)
- 0 == 0; 为true
9. {} 和 [] 的 valueOf() 和 toString() 的结果是什么?
简单介绍一下 valueOf
和 toString
🔸 valueOf()
valueOf()
方法返回指定对象的原始值。
JavaScript调用valueOf
方法将对象转换为原始值。你很少需要自己调用valueOf方法;当遇到要预期的原始值的对象时,JavaScript会自动调用它。
🔸 toString()
toString()
方法返回一个表示该对象的字符串。
每个对象都有一个 toString()
方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString()
方法被每个 Object
对象继承。
可以使用 toString()
检测对象类型:
var a = Object.prototype.toString;
console.log(a.call("aaa"));
console.log(a.call(1));
console.log(a.call(true));
console.log(a.call(null));
console.log(a.call(undefined));
console.log(a.call([]));
console.log(a.call(function() {}));
console.log(a.call({}));
🔸 {}
和[]
的 valueOf()
和 toString()
的结果是什么?
注意:可以在谷歌浏览器里直接通过括号()
,包括值的方式来调用方法
([]).valueOf()
([]).toString()
({}).valueOf()
({}).toString()
或者下面的写法 (有点繁琐)
let emptyObj = {};
console.log('emptyObj.valueOf() :>> ', emptyObj.valueOf()); // {}
console.log('emptyObj.toString() :>> ', emptyObj.toString()); // [object Object]
let emptyArr = [];
console.log('emptyArr.valueOf() :>> ', emptyArr.valueOf()); // []
console.log('emptyArr.toString() :>> ', emptyArr.toString()); // ""
10. JS 的原始值和引用值
在ECMAScript中,变量可以存放两种类型的值,即原始值和引用值。
原始值(primitive value)
:
原始值是固定而简单的值,是存放在栈(stack)
中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。
最新的 ECMAScript 标准定义了7 种原始值:undefined
、Boolean
、Number
、String
、BigInt
、Symbol
和 null
。
其实在今年的早些时候,
null
还不属于原始类型,这个会在下文中贴图展示。
引用值(reference value)
:
引用值则是比较大的对象,存放在堆(heap)
中的对象,也就是说,存储在变量处的值是一个指针(pointer)
,指向存储对象的内存处。
所有引用类型都集成自Object
。
如果一个值是引用类型的,那么它的存储空间将从堆中分配。由于引用值的大小会改变,所以不能把它放在栈中,否则会降低查询速度。相反,存放变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存放在栈中对变量性能无任何负面影响。如图:
我们看一下MDN对原始值的定义:
原始值( primitive values )
除 Object
以外的所有类型都是不可变的(值本身无法被改变)。例如,与 C 语言不同,JavaScript
中字符串是不可变的(译注:如,JavaScript
中对字符串的操作一定返回了一个新字符串,原始字符串并没有被改变)。我们称这些类型的值为“原始值”
。
11. JS 原始值转换的抽象操作 toPrimitive
该题内容已详细记录在这篇文章中:掌握js类型转换,先来学习js原始值转换的抽象操作 toPrimitive
Symbol.toPrimitive
是一个内置的抽象操作,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。
该函数被调用时,会被传递一个字符串参数,表示要转换到的原始值的预期类型。参数的取值是 "number"
、"string"
和 "default"
中的任意一个。
toPrimitive
转换规则如下:
如果传入参数是string
,也就是对象到字符串的转换,经过了如下步骤:
- 如果对象中有
toString()
方法,则调用这个方法。如果它返回一个原始值(undefined、Boolean、Number、String、BigInt、Symbol 和 null)
,js将这个值转换为字符串(如果本身不是字符串的话),并返回这个字符串结果。 - 如果对象没有
toString()
方法,或者toString()
没有返回一个原始值,那么js会调用valueOf()
方法。如果返回值是原始值,js将这个值转换为字符串,并返回字符串结果。 - 否则,js抛出一个类型错误异常。
如果传入参数是number/default
,也就是对象到数字的转换,经过了如下步骤:
和上面有点不同,到数字的转换会先尝试使用valueOf()
方法
- 如果对象具有
valueOf()
方法,后者返回一个原始值,则js会将其转换为数字(如果需要的话)并返回这个数字。 - 否则,如果对象具有
toString()
方法,返回一个原始值(字符串直接量),则js将其转换为数字类型,并返回这个数字。 - 否则,js抛出一个类型错误异常。
JavaScript基础
1. let、const 以及 var 的区别是什么?
(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
2. 在 JS 中什么是变量提升?什么是暂时性死区?
变量提升就是变量在声明之前就可以使用,值为undefined。
在代码块内,使用 let/const 命令声明变量之前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。
3. 为什么0.1+0.2 ! == 0.3,如何处理
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
JS中如何进入浮点数运算?
使用bignumber进行运算,BigNumber.js是一个用于任意精度计算的js库。
那为什么会出现这样的结果呢?
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1和0.2的二进制都是四位无线循环的数。
JS采用的浮点数标准却会裁剪掉我们的数字。
这些循环的数字被裁剪了,就会出现精度丢失的问题,也就造成了 0.1 不再是 0.1 了,而是变成了 0.100000000000000002
0.100000000000000002 === 0.1 // true
那么同样的,0.2 在二进制也是无限循环的,被裁剪后也失去了精度变成了 0.200000000000000002
0.200000000000000002 === 0.2 // true
所以这两者相加不等于 0.3 而是 0.300000000000000004
4. for of
, for in
和 forEach
,map
的区别
for…in
以原始插入顺序访问对象的可枚举属性,包括从原型继承而来的可枚举属性。for ... in
是为遍历对象属性而构建的,不建议与数组一起使用,因为输出的顺序是不固定的。
let obj = {
a:123,
b:"abc"
}
for(let pro in obj){
console.log(pro+':' + obj[pro])
}
//a:123
//b:abc
for…of
语句在可迭代对象(Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,为每个不同属性的值执行语句。
let arr = [123,'abc']
for(let item of arr){
console.log(item)
}
//123
//abc
for…of与for…in的区别
使用for…in循环时,获得的是数组的下标;使用for…of循环时,获得的是数组的元素值。
forEach()
方法对数组的每个元素执行一次给定的函数。
const array1 = ['a', 'b', 'c'];
array1.forEach(element => console.log(element));
// expected output: "a"
// expected output: "b"
// expected output: "c"
map()
方法创建一个新数组,其结果为这个新数组中的每个元素是调用一次提供的函数后的返回值。
const array1 = [1, 4, 9, 16];
// pass a function to map
const map1 = array1.map(x => x * 2);
console.log(map1);
// expected output: Array [2, 8, 18, 32]
5. this 指向问题
- 在函数体中,严格模式下,函数内的
this
会被绑定到undefined
上,在非严格模式下则会被绑定到全局对象window/globa
l上。 - 使用
new
方法调用构造函数时,构造函数内的this
会被绑定到新创建的对象上。 - 通过
call/apply/bind
方法显式调用函数时,函数体内的this
会被绑定到指定参数的对象上。 - 通过上下文对象调用函数时,函数体内的
this
会被绑定到该对象上。 - 在箭头函数中,箭头函数没有自己的
this
,箭头函数的this就是上下文中定义的this。 - 在定时器中,
this
指向window
,因为定时器中采用回调函数作为处理函数,而回调函数的指向window
定时器 setTimeout()
中函数里的this
默认是window
:
const obj = {
index: 1,
a: function () {
console.log(this.index)
window.setTimeout(function name() {
console.log('定时器 this.index', this.index);
console.log('定时器 this', this);
}, 1000)
}
}
obj.a()
// 执行情况:
1
定时器 this.index undefined
定时器 this Window {0: Window, window: Window, self: Window, document: document, name: '', location: Location, …}
我们也可以通过箭头函数使定时器里的this
和外层的this
保持一致:
const obj = {
index: 1,
a: function () {
console.log(this.index)
window.setTimeout(() => {
console.log('定时器 this.index', this.index);
console.log('定时器 this', this);
}, 1000)
}
}
obj.a()
// 执行情况:
1
定时器 this.index 1
定时器 this {index: 1, a: ƒ}
在箭头函数中this的一个常见情况:对象字面量里的箭头函数
var name = 'window';
var A = {
name: 'A',
sayHello: () => {
console.log(this.name)
}
}
A.sayHello();// 以为输出A ? 错啦,其实输出的是window
我相信在这里,大部分同学都会出错,以为sayHello是绑定在A上的,但其实它绑定在window上的,那到底是为什么呢?
一开始,我重点标注了“该函数所在的作用域指向的对象”,作用域是指函数内部,这里的箭头函数,也就是sayHello,所在的作用域其实是最外层的js环境,因为没有其他函数包裹;然后最外层的js环境指向的对象是winodw对象,所以这里的this指向的是window对象。
参考:https://zhuanlan.zhihu.com/p/57204184
6. 谈谈你对 JS 执行上下文栈和作用域链的理解。
该题内容已详细记录在这篇文章中:谈谈你对JS执行上下文栈和作用域链的理解_前端圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/117743992
7. 什么是闭包?闭包的作用是什么?闭包的使用场景?闭包的性能问题?
什么是闭包?
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。
闭包的作用?
可以读取到另个一函数内部的变量,让这些变量的值始终保存在内存中。
闭包的使用场景?
用闭包模拟私有方法,封装私有变量,实现JS模块
编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。
而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern):
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.increment,Counter.decrement 和 Counter.value。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。
上文我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量Counter。我们可以把这个函数储存在另外一个变量makeCounter中,并用他来创建多个计数器。来看下面的例子:
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
闭包的性能问题?
对内存消耗有负面影响,闭包会导致原始作用域链不释放(因为内部函数保存了对外部变量的引用),造成内存泄漏。
对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域长度。
8. call、apply 和 bind 方法的用法、区别和使用场景
该题内容已详细记录在这篇文章中::call、apply和bind方法的用法、区别和使用场景_前端圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/117926588
9. 谈谈你对原型的理解?什么是原型链?原型链解决的是什么问题?prototype 和 proto 区别是什么?
该题内容已详细记录在这篇文章中::谈谈你对原型的理解?什么是原型链?【原型链解决的是什么问题?】prototype 和 proto 区别是什么?_前端圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/118562043
10. 聊一聊 DOM 事件响应机制,事件流,事件冒泡和事件捕获是什么?现代浏览器默认的响应机制是什么?
🔸 什么是事件流?
事件发生时会在元素节点与根节点之间按照特定的顺序传播,路径所经过的所有节点都会收到该事件,这个传播过程即DOM事件流。
事件流是事件在目标元素和顶层元素间的触发顺序, 在早期,微软和网景实现了相反的事件流,网景主张捕获方式,微软主张冒泡方式:
- 事件捕获:事件由最顶层逐级向下传播,直至到达目标元素。
- 事件冒泡:顾名思义,类似水中冒泡,从下往上。事件由第一个被触发的元素接收,然后逐级向上传播。
后来 w3c
采用折中的方式, 规定先捕获再冒泡平息了战火。如此一个事件就被分成了三个阶段(是的, 不光是捕获和冒泡):
- 捕获阶段 - 事件从最顶层元素 window 一直传递到目标元素的父元素.
- 目标阶段 - 事件到达目标元素. 如果事件指定不冒泡. 那就会在这里中止.
- 冒泡阶段 - 事件从目标元素父元素向上逐级传递直到最顶层元素 window. 及捕获阶段的反方向.
🔸 那这里又有一个新的疑问❓❓❓ 既然捕获和冒泡阶段都会触发事件, 那先捕获再冒泡, 岂不是路径上的元素都会触发两次事件?
在 DOM2 中, 事件监听 addEventListener()
的第三个参数 决定事件了是在捕获阶段生效还是在冒泡阶段生效, 接下来简要学习下 addEventListener
。
还有一个小问题, 为什么要单独区分一个目标阶段? 下面会做出解答。
addEventListener
EventTarget.addEventListener()
方法将指定的监听器注册到目标元素上, 当该对象触发指定的事件时, 指定的回调函数就会被执行。addEventListener
有三个参数:
element.addEventListener(event, function, useCapture)
event
: 必须。字符串,指定事件名。function
: 必须。指定要事件触发时执行的函数。useCapture
:可选
。布尔值,指定事件是否在捕获或冒泡阶段执行。
useCapture
的可能值:
true
- 事件句柄在捕获阶段执行false
- 默认。事件句柄在冒泡阶段执行
一个默认的事件监听:
document.getElementById("myBtn").addEventListener("click", function() {
myFunction(p1, p2);
});
🔸 注意:❗ ❗ ❗ 对于目标元素上的事件监听器来说, 事件会处于目标阶段, 而不是冒泡阶段或者捕获阶段. 在目标阶段的事件会触发该元素上的所有监听器, 而不在乎这个监听器到底在注册时 useCapture
是 true
还是 false
.
🔸 现代浏览器默认的响应机制是什么?
我们注意到addEventListener()
的第三个参数是可选的,而默认值是false
,除非主动的设置该参数为true
,否则 我们添加的事件监听器默认情况就是在冒泡阶段执行,如此 在某种程度上是不是可以说现代浏览器默认的响应机制是 事件冒泡(仅个人说法
)
11. 谈谈你对严格模式 “use strict” 的理解
🔶 什么是严格模式 "use strict"
?
使用 严格模式 会对脚本 或者 函数 启动更加严格的检查来避免失误引起的错误。
🔶 为脚本开启严格模式
为整个脚本文件开启严格模式,需要在所有语句之前放一个特定语句 "use strict";
(或 'use strict';
)
// 整个脚本都开启严格模式的语法
"use strict";
var v = "Hi! I'm a strict mode script!";
这种语法存在陷阱:不能盲目的合并冲突代码。试想合并一个严格模式的脚本和一个非严格模式的脚本:合并后的脚本代码看起来是严格模式。反之亦然:非严格合并严格看起来是非严格的。合并均为严格模式的脚本或均为非严格模式的都没问题,只有在合并严格模式与非严格模式有可能有问题。建议按一个个函数去开启严格模式(至少在学习的过渡期要这样做)。
🔶 为函数开启严格模式
同样的,要给某个函数开启严格模式,得把 "use strict";
声明一字不漏地放在函数体所有语句之前。
function doSomething(val) {
"use strict";
x = val + 10;
}
🔶 举例严格模式的一些规则:
🔸 全局变量显式声明
在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。
"use strict";
a = 1;
上面的写法报错:Uncaught ReferenceError: a is not defined
🔸 变量都必须先声明,再使用
严格模式下,变量都必须先用var命令声明,然后再使用。
function doSomething() {
"use strict";
x = 10;
console.log(x);
}
doSomething()
上面的写法报错:Uncaught ReferenceError: x is not defined
非严格模式下:
function doSomething() {
x = 10;
console.log(x);
}
doSomething()
上面的写法会正常输出 10 ,不会报错。
🔸 重名错误
严格模式下:
- 对象不能有重名的属性
- 函数不能有重名的参数
🔸 禁止删除变量
严格模式下无法删除变量。
"use strict";
var x;
delete x; // 语法错误
var o = Object.create(null, {
'x': {
value: 1,
configurable: true
}
});
delete o.x; // 删除成功
只有configurable设置为true的对象属性,才能被删除。
🔸 保留字
为了向将来Javascript的新版本过渡,严格模式新增了一些保留字:implements, interface, let, package, private, protected, public, static, yield。
使用这些词作为变量名将会报错。
function package(protected) { // 语法错误
"use strict";
var implements; // 语法错误
}
🔸 this指向问题
严格模式下,函数内的 this
会被绑定到 undefined
上,在非严格模式下则会被绑定到全局对象window/global
上。
var a = 100;
function f1() {
console.log(this);
}
console.group("正常模式下:")
f1();
console.groupEnd();
"use strict";
var a = 100;
function f1() {
console.log(this);
}
console.group("严格模式下:")
f1();
console.groupEnd();
12. 常见的DOM操作有哪些
🔶 DOM 节点的获取
DOM 节点的获取的API及使用:
getElementById // 按照 id 查询
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询
// 按照 id 查询
var imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素
// 按照标签名查询
var pList = document.getElementsByTagName('p') // 查询到标签为 p 的集合
console.log(divList.length)
console.log(divList[0])
// 按照类名查询
var moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合
// 按照 css 选择器查询
var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合
🔶 DOM 节点的创建
创建一个新节点,并把它添加到指定节点的后面。 已知的 HTML 结构如下:
<div id="container">
<h1 id="title">我是标题</h1>
</div>
要求添加一个有内容的 span
节点到 id 为 title 的节点后面,做法就是:
// 首先获取父节点
var container = document.getElementById('container')
// 创建新节点
var targetSpan = document.createElement('span')
// 设置 span 节点的内容
targetSpan.innerHTML = 'hello world'
// 把新创建的元素塞进父节点里去
container.appendChild(targetSpan)
🔶 DOM 节点的删除
删除指定的 DOM 节点, 已知的 HTML 结构如下:
<div id="container">
<h1 id="title">我是标题</h1>
</div>
需要删除 id 为 title 的元素,做法是:
// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = document.getElementById('title')
// 删除目标元素
container.removeChild(targetNode)
或者通过子节点数组来完成删除:
// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = container.childNodes[1]
// 删除目标元素
container.removeChild(targetNode)
注意,childNodes[1]
的下表是1
,而不是0
,0
是<div id="container">
里最上面的文本。
比如下面的html结构:
<div id="container">
文本1
<h1 id="title">我是标题</h1>
文本2
</div>
打印<div id="container">
的childNodes
:
var container = document.getElementById('container')
console.log(container.childNodes);
分别展开0和2的对象:
所以,childNodes[1]
才是目标的dom节点,就算节点前后都没有文本,那表示文本为空,并不是说没有文本:
🔶 新增节点和移动节点
var div1 = document.getElementById('div1')
// 添加新节点
var p1 = document.createElement('p')
p1.innerHTML = 'this is p1'
div1.appendChild(p1) // 添加新创建的元素
// 移动已有节点。注意,这里是“移动”,并不是拷贝
var p2 = document.getElementById('p2')
div1.appendChild(p2)
🔶 获取父元素
var div1 = document.getElementById('div1')
var parent = div1.parentElement
13. 数组有哪些原生方法
详细讲解已另开新的文章:JS 数组有哪些原生方法,那么多 API 真的很难记住吗?
数组原生方法梳理
🔶 数组判断方法:Array.isArray()
🔶 数组合并的方法:concat()
,返回的是拼接好的数组,不影响原数组。
🔶 数组和字符串的转换方法:toString()
、join()
, 其中 join()
方法可以指定转换为字符串时的分隔符。
🔶 数组尾部操作的方法: pop()
和 push()
,push()
方法可以传入多个参数。
🔶 数组首部操作的方法: shift()
和 unshift()
;
🔶 数组重排序的方法: reverse()
和 sort()
;sort()
方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则b 会被排列到 a 之前;如果返回值为负数, a 会被排列到 b 之前。
🔶 数组截取办法: slice()
,用于截取数组中的一部分,返回一个新的数组对象,不影响原数组。arr.slice(begin, end)
,slice
会提取原数组中索引从 begin
到 end
的所有元素(包含 begin
,但不包含 end
)。 注意 ❗ ❗ ❗ slice()
方法是浅拷贝,具体在下文做解释。
🔶 数组 删除、替换或插入方法: splice()
,通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容 (注意不是整个数组)
。此方法会改变原数组。
🔶 数组 寻找指定元素的索引方法 indexOf()
和 lastIndexOf()
,用来判断一个元素是否在数组里,或者寻找指定元素出现的所有位置
🔶 数组 迭代方法:forEach()
、map()
、filter()
、every()
和 some()
🔶 数组 归并方法: reduce()
🔶 数组 元素查找方法:indexOf()
、includes()
、find()
和 findIndex()
🔶 数组 扁平化方法: flat()
14. setTimeout 倒计时为什么会出现误差?
setTimeout()
只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在 setTimeout()
指定的时间执行。所以, setTimeout()
的第二个参数表示的是最少时间,并非是确切时间。
HTML5标准规定了 setTimeout()
的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒。在此之前。老版本的浏览器都将最短时间设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常是间隔16毫秒执行。这时使用 requestAnimationFrame()
的效果要好于 setTimeout()
15. 常用的正则表达式有哪些
该题内容已详细记录在这篇文章中:由浅入深的正则表达式教程,简单快速学习正则
16. 对JSON的理解
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
JSON对象包含两个方法: 用于解析 JSON 的 parse()
方法,以及将对象/值转换为 JSON字符串的 stringify()
方法。除了这两个方法, JSON这个对象本身并没有其他作用,也不能被调用或者作为构造函数调用。
◾ JSON.parse()
方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。
const json = '{ "name":"runoob", "alexa":10000, "site":"www.runoob.com" }';
const obj = JSON.parse(json); // {name: 'runoob', alexa: 10000, site: 'www.runoob.com'}
◾ JSON.stringify()
方法将一个 JavaScript 对象或值转换为 JSON 字符串。
console.log(JSON.stringify({ x: 5, y: 6 }));// {"x":5,"y":6}
在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。
17. 为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?
arguments
是一个对应于传递给函数的参数的类数组对象。
arguments
对象是所有(非箭头)函数中都可用的局部变量。你可以使用arguments
对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,第一个参数在索引0处。例如,如果一个函数传递了三个参数,你可以以如下方式引用他们:
arguments[0]
arguments[1]
arguments[2]
arguments
与数组相似,但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。
要遍历类数组,有三个方法:
(1)将数组的方法应用到类数组上,这时候就可以使用call和apply方法,如:
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))
}
JavaScript手写题 (难)
1. 对AJAX的理解,实现一个AJAX请求
该题内容已详细记录在这篇文章中:简洁全面的“手写AJAX,以及用Promise封装AJAX请求“,给你一个满分答案
2. 使用Promise封装AJAX请求
该题内容已详细记录在这篇文章中:简洁全面的“手写AJAX,以及用Promise封装AJAX请求“,给你一个满分答案
3. 什么是深拷贝?深拷贝和浅拷贝有什么区别?
浅拷贝是指只复制第一层对象,但是当对象的属性是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。
深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。
最简单的深拷贝:
JSON.parse(JSON.stringify(obj)): 性能最快
实现一个深拷贝:
function deepClone(obj) { //递归拷贝
if (typeof obj !== 'object' || obj === null) {
//如果不是复杂数据类型 或者为null,直接返回
return obj;
}
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);
let result;
if (obj instanceof Array) {
result = [];
} else {
result = {}
}
for (let key in obj) {
// 判断是否是对象自身的属性,筛掉对象原型链上继承的属性
if (obj.hasOwnProperty(key)) {
//如果 obj[key] 是复杂数据类型,递归
result[key] = deepClone(obj[key]);
}
}
return result;
}
我们应该拷贝要拷贝对象自身的属性,对象原型上的属性我们不应该拷贝,这里我们用到hasOwnProperty()
方法来解决。
hasOwnProperty()
方法会返回一个布尔值,这个方法可以用来检测一个对象是否含有特定的自身属性;该方法会忽略掉那些从原型链上继承到的属性。
4. instanceof 操作符的实现原理及实现
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
instanceof 操作符的实现及验证:
function myInstanceof(left, right) {
// 获取对象的原型
let leftProto = Object.getPrototypeOf(left);
// 获取构造函数的 prototype 对象
let rightPrototype = right.prototype;
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!leftProto) return false;
if (leftProto === rightPrototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
leftProto = Object.getPrototypeOf(leftProto);
}
}
// 验证
var arr = [1, 2, 34, 4];
var out1 = myInstanceof(arr, Array);
console.log(out1);
var out2 = myInstanceof(arr, String);
console.log(out2);
5. call、apply 和 bind 方法的用法、区别和使用场景
该题内容已详细记录在这篇文章中::call、apply和bind方法的用法、区别和使用场景_前端圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/117926588
6. 手写 实现 call、apply 和 bind 方法
该题内容已详细记录在这篇文章中: 手写 实现call、apply和bind方法 超详细!!!_前端圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/118026630
7. new 的原理是什么?new 的模拟实现?通过new 的方式创建对象和通过字面量创建有什么区别?
该题内容已详细记录在这篇文章中:new的原理是什么?new的模拟实现?通过new的方式创建对象和通过字面量创建有什么区别?_前端圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/118504331
8. 详解 JS 继承,从原理入手到 五种 JS 实现继承方式
该题内容已详细记录在这篇文章中:详解JS继承,从原理入手到 五种js 实现继承方式_前端圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/118688053
9. 手写 Object.create
思路:将传入的对象作为原型
function create(obj) {
function F() {}
F.prototype = obj
return new F()
}
参考这篇https://www.cnblogs.com/ranyonsue/p/11201730.html博客,里面一种js实现继承的方式用到的就是上面的实现方式,其实就是手写了一个Object.create方法,来实现了继承。
10. 取数组的最大值(ES5、ES6), 详解Math.max() 和 reduce() API
该题内容已详细记录在这篇文章中:取数组的最大值(ES5、ES6),详解Math.max() 和 reduce() API_圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/119351989
ES6+部分
1. 箭头函数的this指向哪里
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。
可以⽤Babel理解⼀下箭头函数:
// ES6
const obj = {
getArrow() {
return () => {
console.log(this === obj);
};
}
}
转化后:
// ES5,由 Babel 转译
var obj = {
getArrow: function getArrow() {
var _this = this;
return function () {
console.log(_this === obj);
};
}
};
2. 箭头函数与普通函数的区别
(1)箭头函数比普通函数更加简洁
- 如果没有参数,就直接写一个空括号即可
- 如果只有一个参数,可以省去参数的括号
- 如果有多个参数,用逗号分割
- 如果函数体的返回值只有一句,可以省略大括号
- 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
let fn = () => void doWhat();
(2)箭头函数没有自己的this
箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。
(3)箭头函数继承来的this指向永远不会改变
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
new obj.a() // undefined
new obj.b() // Uncaught TypeError: obj.b is not a constructor
对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}
是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。
(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向
var id = 'Global';
let fun1 = () => {
console.log(this.id)
};
fun1(); // 'Global'
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'
(5)箭头函数不能作为构造函数使用
构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
(6)箭头函数没有自己的arguments
箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
(7)箭头函数没有prototype
3. 对 rest 参数(剩余参数) 与 Spread 语法(扩展语法)的理解
该题内容已详细记录在这篇文章中:对 rest 参数(剩余参数) 与 Spread 语法(展开语法)的理解_前端圆圆-CSDN博客 https://blog.csdn.net/weixin_45844049/article/details/118575357
4. 对象属性简写
在ES6中允许我们在设置一个对象的属性的时候不指定属性名。
不使用ES6:
const name='Ming',age='18',city='Shanghai';
const student = {
name:name,
age:age,
city:city
};
console.log(student);//{name: "Ming", age: "18", city: "Shanghai"}
对象中必须包含属性和值,显得非常冗余。
使用ES6:
const name='Ming',age='18',city='Shanghai';
const student = {
name,
age,
city
};
console.log(student);//{name: "Ming", age: "18", city: "Shanghai"}
对象中直接写变量,非常简洁。
5. const对象的属性可以修改吗
const
保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const
只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。
6. 什么是Babel,Babel能做什么
官方网站这样讲:
Babel 是一个 JavaScript 编译器。
javascript在不断的发展,各种新的标准和提案层出不穷,但是由于浏览器的多样性,导致可能几年之内都无法广泛普及,babel可以让你提前使用这些语言特性,他是一种用途很多的javascript编译器,他把最新版的javascript编译成当下可以执行的版本,简言之,利用babel就可以让我们在当前的项目中随意的使用这些新最新的es6,甚至es7的语法。说白了就是把各种javascript千奇百怪的语言统统专为浏览器可以认识的语言。
Axios和Fetch
1. fetch 如何请求数据
该题内容已详细记录在这篇文章中:越来越火的Fetch是如何请求数据的?与Ajax和Axios又有什么区别_圆圆
2. Fetch与Ajax和Axios有什么区别
该题内容已详细记录在这篇文章中:越来越火的Fetch是如何请求数据的?与Ajax和Axios又有什么区别_圆圆
浏览器
浏览器本地存储
前端常用的本地存储方式有三种:cookie
、localStorage
、sessionStorage
- cookie:在HTML5标准前本地储存的主要方式,优点是兼容性好,缺点是大小只有4k,自动请求头加入cookie浪费流量,使用起来麻烦需要自行封装;
HTML5 提供了 sessionStorage (会话存储) 和 localStorage(本地存储)两个存储对象来对网页的数据进行添加、删除、修改、查询操作。
-
sessionStorage
用于临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页之后将会删除这些数据,在浏览器打开期间存在,包括页面重新加载 -
localStorage
用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去除。
三者的具体对比如下:
cookie使用起来麻烦需要自行封装。
localStorage和sessionStorage使用相同的API:
其他
B/S架构和C/S架构是什么
C/S
即Client/Server
(客户机/服务器)结构。
B/S
即Browser/Server
(浏览器/服务器)结构,就是只安装维护一个服务器(Server),而客户端采用浏览器(Browse)运行软件。
C/S开发维护成本高于B/S。因为采用C/S结构时,对于不同的客户端要开发不同的程序,而且软件安装调试和升级都需要在所有客户机上进行。
C/S安全性高。C/S适用于专人使用的系统,可以通过严格的管理派发软件。
B/S使用人数多,不固定,安全性低。
参考
- Fetch API 教程 - 阮一峰的网络日志 (ruanyifeng.com)
- ES6、ES7、ES8、ES9、ES10新特性一览
- 「2021」高频前端面试题汇总之JavaScript篇(上)
- DOM 事件流 - 简书 (jianshu.com)
- Javascript 严格模式详解 - 阮一峰的网络日志 (ruanyifeng.com)
- JS原始值转换算法—toPrimitive() – 宿雪冷音的博客 (suxuewb.cn)
- Javascript–原始值和引用值 - Localhost - 博客园 (cnblogs.com)
- JavaScript中对象到字符串、数字的转换过程_-ELZ
- 什么是Babel,Babel能做什么 - 简书 (jianshu.com)
- fetch 如何请求数据