近期整理了一下高频的前端面试题,分享给大家一起来学习。如有问题,欢迎指正!
1. JavaScript有哪些数据类型
总共有8种数据类型,分别是Undefined
、Null
、Boolean
、Number
、String
、Object
、Symbol
、BigInt
Null 代表的含义是空对象,主要用于赋值给一些可能会返回对象的变量,作为初始化
Undefined 代表的含义是未定义,一般变量声明了但还没有定义值的时候就会返回undefined
Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突问题
BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用BigInt可以安全的存储和操作大整数,即使这个数已经超出了Number能够表示的安全整数范围
- 原始数据类型:Undefined、Null、Boolean、Number、String、Symbol、BigInt
- 引用数据类型:Object、Array、Function、RegExp、Date、Global、Math等
原始数据类型与引用数据类型的区别
原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,数据被频繁使用的数据,可以放入栈中存储
引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。在栈中存储指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
2. 数据类型检测方式有哪些
1. typeof
typeof
可以区分除了Null
类型以外的其他基本数据类型,以及从对象类型中识别出函数(function)。
其返回值有:number
、string
、boolean
、undefined
、symbol
、bigint
、function
、object
。
其中, typeof null
返回 "object"
如果要识别null
,可直接使用===
全等运算符来判断
console.log(typeof 2); // number
console.log(typeof 'str'); // string
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof Symbol()); // symbol
console.log(typeof BigInt(2782371)); // bigint
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof null); // object
2. instanceof
instanceof
一般用来判断引用数据类型,但不能正确判断基本数据类型,根据在原型链中查找判断当前数据的原型对象是否存在,返回布尔类型值
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
let date = new Date();
console.log(date instanceof Date); // true
instanceof实现方式
function myInstanceof(left, right) { // 获取对象的原型 let proto = Object.getPrototypeOf(left) // 获取构造函数的 prototype 对象 let prototype = right.prototype; // 判断构造函数的 prototype 对象是否在对象的原型链上 while (true) { if (!proto) return false; if (proto === prototype) return true; // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型 proto = Object.getPrototypeOf(proto); } }
3. constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
constructor
有两个作用,一是判断数据的类型,二是对象实例通过constructor
对象访问它的构造函数。
需要注意,如果创建一个对象来改变它的原型,
constructor
就不能用来判断数据类型function Fn(){}; Fn.prototype = new Array(); const f = new Fn(); console.log(f.constructor===Fn); // false console.log(f.constructor===Array); // true
4. Object.prototype.toString.call()
Object.prototype.toString.call()
使用Object对象的原型方法toString来判断数据类型
const objectJudgment = value => Object.prototype.toString.call(value).slice(8, -1);
console.log(Object.prototype.toString.call(1)) // [object Number]
console.log(objectJudgment(2)); // Number
console.log(objectJudgment(true)); // Boolean
console.log(objectJudgment('str')); // String
console.log(objectJudgment([])); // Array
console.log(objectJudgment(function(){})); // Function
console.log(objectJudgment({})); // Object
console.log(objectJudgment(undefined)); // Undefined
console.log(objectJudgment(null)); // Null
console.log(objectJudgment(/123/g)); // RegExp
console.log(objectJudgment(new Date())); // Date
console.log(objectJudgment(document)); // HTMLDocument
console.log(objectJudgment(window)); // Window
3. 判断数组的方式
- 通过
Object.prototype.toString.call()
做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
- 通过原型链做判断
obj.__proto__ === Array.prototype;
- 通过ES6的
Array.isArray()
做判断
Array.isArrray(obj);
- 通过instanceof做判断
obj instanceof Array
- 通过
Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)
4. isNaN
和 Number.isNaN
函数的区别
-
isNaN
函数会首先尝试将这个参数转换为数值,然后才会对转换后的结果是否是NaN
进行判断,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。 -
函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
5. ||
和&&
操作符的返回值
||
和&&
首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔值,然后再执行条件判断
-
||
:如果条件判断结果为true
,就返回第一个操作数的值,如果为false
就返回第二个操作数的值 -
&&
:如果条件判断结果为true
,就返回第二个操作数的值,如果为false
就返回第一个操作数的值
6. Object.is()
与比较操作符==
、===
的区别
-
使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
-
使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
-
使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
7. 如何 遍历对象的属性
-
遍历自身可枚举的属性(可枚举、非继承属性):
Object.keys
,该方法会返回一个由给定对象的自身可枚举属性组成的数组 -
遍历自身的所有属性(可枚举、不可枚举、非继承属性):
Object.getOwnPropertyNames()
,该方法会返回一个由指定对象的所有自身属性组成的数组 -
遍历 可枚举的自身属性和继承属性:
for...in...
8. for...in
和for...of
的区别
for...in
获取的是对象的键名,for...of
获取的是对象的键值for...in
会遍历对象的整个原型链,性能非常差不推荐使用,for...of
只会遍历当前对象不会遍历原型链- 对于数组的遍历,
for...in
会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for...of
只会返回数组的下标对应的属性值
总结
for...in
循环主要是为了遍历对象,不适用于遍历数组;for...of
循环可以用来遍历数组、类数组对象、字符串、Set
、Map
、Generator
对象
9. JavaScript中的遍历方法
方法 | 特点 |
---|---|
for | for 循环有三个表达式组成,分别是声明循环变量、判断循环条件、更新循环变量。这三个表达式用分号分隔,都可以省略,但中间的分号不能省略。在执行时候,会先判断执行条件,再执行。可以用来 遍历数组、字符串、类数组、DOM节点等 |
while | while 循环中的结束条件可以是各种类型,但是始终都会转为增加值,转换规则:Boolean –>true为真,false为假;String –>空字符串为假,其余为真;Number –>0为假,其余为真;Null、Undefined、NaN 全为假;Object 全为真。在执行的时候,会先判断,再执行。 |
do/while | 先执行再判断,即使初始条件不成立,do/while 循环至少执行一次。 |
for await…of | for await...of 方法被称为异步迭代器,该方法是主要用来遍历异步对象。for await...of 语句会在异步或者同步可迭代对象上创建一个迭代循环,包括 String ,Array ,类数组,Map , Set 和自定义的异步或者同步可迭代对象。这个语句只能在 async function 内使用。 |
map() | map() 方法会返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。该方法按照原始数组元素顺序依次处理元素。注意: map() 不会对空数组进行检测,它会返回一个新数组,不会改变原始数组。该方法的第一个参数为回调函数,他有三个参数:currentValue ( 必须。当前元素的值)、index (可选。当前元素的索引值)、arr (可选。当前元素属于的数组对象)。第二个参数用来绑定参数函数内部的this变量,可选。 |
forEach() | forEach 方法用于调用数组的每个元素,并将元素传递给回调函数。数组中的每个值都会调用回调函数,回调函数有三个参数:currentValue (必需。当前元素)、index (可选。当前元素的索引值)、arr ( 可选。当前元素所属的数组对象)。该方法还可以有第二个参数,用来绑定回调函数内部this变量(前提是回调函数不能是箭头函数,因为箭头函数没有this)。需要注意的是,forEach方法不会改变原数组,也没有返回值。 |
filter() | filter() 方法用于过滤数组,满足条件的元素会被返回。它的参数是一个回调函数,所有数组元素依次执行该函数,返回结果为true的元素会被返回。该方法会返回一个新的数组,不会改变原数组。该方法的第一个参数是回调函数,它有三个参数:currentValue (必须。当前元素的值)、index (可选。当前元素的索引值)、arr (可选。当前元素属于的数组对象)。该方法有第二个参数,用来绑定参数函数内部的this变量。 |
every() 和some() | 数组方法,some() 只要有一个是true,即返回true;而every() 只要有一个是false,即返回false |
find() 和findIndex() | 数组方法find() 返回的是第一个符合条件的值;findIndex() 返回的是第一个返回条件的值的索引值 |
reduce() 和reduceRight() | 数组方法,reduce() 对数组正序操作;reduceRight() 对数组逆序操作 |
Object.keys() 和Object.getOwnPropertyNames() | Object.keys() 获取对象可枚举的属性;Object.getOwnPropertyNames() 返回所有属性,两者都返回一个包含属性名的数组 |
10. 强制类型转换和隐式类型转换
强制
-
转换成字符串:
1.toString()
、String(1)
-
转换成数字:
Number("2")
、parseInt()
、parseFloat()
-
转换成布尔:
Boolean()
隐式
+
1 + '23' // '123'
1 + false // 1
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' + false // '1false'
false + true // 1
-
、*
、/
1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN
==
3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
11. JavaScript
的作用域和作用域链
作用域:即变量和函数生效的区域或集合。作用域决定了代码块中变量和其他资源的可见性。一般分为全局作用域、局部作用域
作用域链:当在 JS 中使用一个变量时,JS 引擎会尝试在当前作用域下寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推,直至找到该变量或是查找至全局作用域,如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。
全局作用域:定义的变量可以被任何地方访问。
-
最外层定义的函数和最外层函数之外定义的变量拥有全局作用域
-
window上的属性都具有全局作用域,在JavaScript中,window == lobal
局部作用域:与全局作用域相反,局部作用域内的变量只能被定义它的局部作用域访问
-
函数作用域:内的变量和函数只能由函数内部访问。作用域是分层的,内部函数可以访问外部函数作用域里的变量,反之不可以
-
块级作用域:凡是代码块就可以划分变量的作用域,这种作用域的规则就叫做块级作用域。
12. 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声明变量必须设置初始值。
7. 指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
13. new
操作符具体做了什么?如何实现?
- 首先创建一个新的空对象
- 设置原型,让对象的原型设置为函数的
prototype
对象 - 让函数的this指向这个对象,执行构造函数的代码
- 判断函数的返回值类型,如果是值类型,返回创建的对象;如果是引用类型,就返回这个引用类型的对象
实现代码
function objectFactory() {
let newObject = null;
let constructor = Array.prototype.shift.call(arguments);
let result = null;
// 参数判断
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 新建一个空对象,对象的原型为构造函数的 prototype 对象
newObject = Object.create(constructor.prototype);
// 将 this 指向新建对象,并执行函数
result = constructor.apply(newObject, arguments);
// 判断返回对象
let flag =
result && (typeof result === "object" || typeof result === "function");
// 判断返回结果
return flag ? result : newObject;
}
12. Map
与Object
的区别
Map | Object | |
---|---|---|
意外的键 | Map 默认情况不包含任何键,只包含显示插入的键 | Object 有一个原型,原型链上的键名有可能和自己在对象上设置的键名产生冲突 |
键的类型 | Map 的键可以是任意值,包括函数、对象或任意基本类型 | Object 的键必须是String 或者Symbol |
键的顺序 | Map 中的key 是有序的。因此,当迭代的时候,Map对象以插入的顺序返回键值 | Object 的键是无序的 |
Size | Map 的键值对个数可以轻易的通过size属性获取 | Object 的键值对个数只能手动计算 |
迭代 | Map 是iterable 的,所以可以直接被迭代 | 迭代Object 需要以某种方式获取它的键然后迭代 |
性能 | 在频繁增删键值对的场景下表现更好 | 在频繁增删键值对的场景下未作出优化 |
15. JavaScript
的内置对象
(1)值属性:这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity
、NaN
、undefined
、globalThis
(2)函数属性:全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()
、uneval()
、isFinite()
、isNaN()
、parseFloat()
、parseInt()
、decodeURI()
、decodeURIComponent()
、encodeURI()``encodeURIComponent()
(3)基本对象:基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object
、Function
、Boolean
、Symbol
(4)错误对象:错误对象是一种特殊的基本对象。它们拥有基本的 Error
类型,同时也有多种具体的错误类型。例如 Error
、AggregateError
、EvalError
、InternalError
、RangeError
、ReferenceError
、SyntaxError
、TypeError
、URIError
(5)数字和日期对象:用来表示数字、日期和执行数学计算的对象。例如 Number
、BigInt
、Math
、Date
(6)字符串:用来表示和操作字符串的对象。例如 String
、RegExp
(7)可索引的集合对象:这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
(8)使用键的集合对象:这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 例如 Map
、Set
、WeakMap
、WeakSet
(9)结构化数据:这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON
、ArrayBuffer
、DateView
、Atomics
(10)控制抽象对象:控件抽象可以帮助构造代码,尤其是异步代码。例如 Promise
、Generator
、GeneratorFunction
、AsyncFunction
(11)反射 例如 Reflect
、Proxy
(12)国际化:ECMAScript 核心的附加功能,用于支持多语言处理。例如 Intl
、Intl.Collator
、Intl.DateTimeFormat
、Intl.ListFormat
、Intl.NumberFormat
、Intl.PluralRules
、Intl.RelativeTimeFormat
、Intl.Locale
(13)WebAssembly 例如 - WebAssembly
、WebAssembly.Module
、WebAssembly.Instance
、WebAssembly.Memory
、WebAssembly.Table
、WebAssembly.CompileError
、WebAssembly.RuntimeError
(14)其他 例如 arguments
16. 对JSON
的理解
JSON
对象包含两个方法:用于解析 JSON
的 parse()
方法,以及将对象/值转换为 JSON 字符串的 stringify()
方法。除了这两个方法,JSON 这个对象本身并没有其他作用,也不能被调用或者作为构造函数调用。JSON 是一种语法,用来序列化对象、数组、数值、字符串、布尔值和 null 。
JSON.stringify()
方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个replacer
函数,则可以选择性地替换值,或者指定的replacer
是数组,则可选择性地仅包含数组指定的属性。当在循环引用时会抛出异常TypeError
;当尝试去转换BigInt
类型的值会抛出TypeError
。
- 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。
- 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
- 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
undefined
、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成null
(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){})
orJSON.stringify(undefined)
.- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
- 所有 以 symbol 为属性键的属性都会被完全忽略掉,即便
replacer
参数中强制指定包含了它们。- Date 日期调用了 toJSON() 将其转换为了 string 字符串(同 Date.toISOString()),因此会被当做字符串处理。
- NaN 和 Infinity 格式的数值及 null 都会被当做 null。
- 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
JSON.parse()
方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换 (操作)。若传入的字符串不符合 JSON 规范,则会抛出SyntaxError
异常。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。
17. 数组的原生方法
-
数组和字符串的转换方法:
toString()
、toLocalString()
、join()
其中join()
方法可以指定转换为字符串时的分隔符。 -
数组尾部操作的方法
pop()
和push()
,push
方法可以传入多个参数。 -
数组首部操作的方法
shift()
和unshift()
重排序的方法reverse()
和sort()
,sort
方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。 -
数组连接的方法
concat()
,返回的是拼接好的数组,不影响原数组。 -
数组截取办法
slice()
,用于截取数组中的一部分返回,不影响原数组。 -
数组插入方法
splice()
,影响原数组查找特定项的索引的方法,indexOf()
和lastIndexOf()
迭代方法every()
、some()
、filter()
、map()
和forEach()
方法 -
数组归并方法
reduce()
和reduceRight()
方法 -
数组取值的方法
at()
,接收一个整数值并返回该索引对应的元素,允许正数和负数。负整数从数组中的最后一个元素开始倒数
18. this
的理解
this
是 JS 的一个关键字,它是函数运行时,自动生成的一个内部对象,只能在函数内部使用,随着函数使用场合的不同,this
的值会发生变化,但有一个总的原则:this指的是调用函数的那个对象
。
this的绑定规则: 默认绑定、隐式绑定、显示绑定、new绑定、ES6新增箭头函数绑定
默认绑定:通常是指函数独立调用,不涉及其他绑定规则;非严格模式下,this指向window,严格模式下,this指向undefined
- 非严格模式:
print()
为默认绑定,this
指向window
,所以打印window
和234
var foo = 123;
function print(){
this.foo = 234;
console.log(this); // window
console.log(foo); // 234
}
print();
- 严格模式:函数内部
this
指向undefined
,但全局对象window
不会受影响
"use strict";
var foo = 123;
function print(){
console.log('print this is ', this); // print this is undefined
console.log(window.foo); // 123
console.log(this.foo); // Uncaught TypeError: Cannot read property 'foo' of undefined
}
console.log('global this is ', this); // global this is Window{...}
print();
- let/const:
let/const
定义的变量存在暂时性死区,而且不会挂载到window
对象上,因此print
中是无法获取到a和b
的
let a = 1;
const b = 2;
var c = 3;
function print() {
console.log(this.a); // undefined
console.log(this.b); // undefined
console.log(this.c); // 3
}
print();
console.log(this.a); // undefined
- 对象内执行:
foo
虽然在obj
的bar
函数中,但foo
函数仍然是独立运行的,foo
中的this
依旧指向window
对象
a = 1;
function foo() {
console.log(this.a);
}
const obj = {
a: 10,
bar() {
foo(); // 1
}
}
obj.bar();
- 自执行函数:
this
指向window
自执行函数只要执行到就会运行,并且只会运行一次,this
指向window
a = 1;
(function(){
console.log(this); // Window{...}
console.log(this.a); // 1
}())
function bar() {
b = 2;
(function(){
console.log(this); // Window{...}
console.log(this.b); // 2
}())
}
bar();
隐式绑定:函数的调用是在某个对象上触发的,即调用位置存在上下文对象,通俗点说就是XXX.func()
这种调用模式。此时func
的this
指向XXX
,但如果存在链式调用,例如XXX.YYY.ZZZ.func
,记住一个原则:this永远指向最后调用它的那个对象
- 隐式绑定:
obj
是通过var
定义的,obj
会挂载到window
之上的,obj.foo()
就相当于window.obj.foo()
,这也印证了this永远指向最后调用它的那个对象规则
var a = 1;
function foo() {
console.log(this.a);
}
// 对象简写,等同于 {a:2, foo: foo}
var obj = {a: 2, foo}
foo(); // 1
obj.foo(); // 2
- 对象链式调用
var obj1 = {
a: 1,
obj2: {
a: 2,
foo(){
console.log(this.a)
}
}
}
obj1.obj2.foo() // 2
显示绑定:比较好理解,就是通过call()、apply()、bind()
等方法,强行改变this
指向
上面的方法虽然都可以改变
this
指向,但使用起来略有差别:
call()和apply()
函数会立即执行bind()
函数会返回新函数,不会立即执行函数call()和apply()
的区别在于call
接受若干个参数,apply
接受数组。
function foo () {
console.log(this.a);
};
var obj = { a: 1 };
var a = 2;
foo(); // 2
foo.call(obj); // 1
foo.apply(obj); // 1
foo.bind(obj);
function foo() {
console.log(this.a);
};
function doFoo(fn) {
console.log(this); // { a: 1, foo:f}
fn.call(this); // 1
}
var obj = { a: 1, foo };
var a = 2;
doFoo.call(obj, obj.foo);
new绑定:通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this
function Foo(){
getName = function(){ console.log(1); };
return this;
}
Foo.getName = function(){ console.log(2); };
Foo.prototype.getName = function(){ console.log(3); };
var getName = function(){ console.log(4); };
function getName(){ console.log(5) };
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3
箭头函数:没有自己的this
,它的this
指向外层作用域的this
,且指向函数定义时的this
而非执行时
name = 'tom'
const obj = {
name: 'zc',
intro: () => {
console.log('My name is ' + this.name)
}
}
obj.intro(); // My name is tom
19. 实现call
、apply
、bind
函数
call
函数的实现步骤
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 处理传入的参数,截取第一个参数后的所有参数。
- 将函数作为上下文对象的一个属性。
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性。
- 返回结果。
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};
apply 函数的实现步骤
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 将函数作为上下文对象的一个属性。
- 判断参数值是否传入
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性
- 返回结果
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};
bind 函数的实现步骤
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 保存当前函数的引用,获取其余传入参数值。
- 创建一个函数返回
- 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};
20. 箭头函数与普通函数的区别
- 箭头函数比普通函数更加简洁
- 箭头函数没有自己的this
- 箭头函数继承来的this指向永远不会改变
- call()、apply()、bind()等方法不能改变箭头函数中this的指向
- 箭头函数不能作为构造函数使用
- 箭头函数没有自己的arguments
- 箭头函数没有prototype
- 箭头函数不能用作Generator函数,不能使用yelid关键字
21. 浅拷贝和深拷贝的实现
浅拷贝:如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。即浅拷贝是拷贝一层
,深层次的引用类型则共享内存地址。常用的方法有:object.assgin,扩展运算符等等
var a = { count: 1, deep: { count: 2 } };
var b = Object.assign({}, a);
// 或者
var c = {...a};
// 实现一个浅拷贝
function shallowClone(obj) {
const newObj = {};
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
newObj[prop] = obj[prop];
}
}
return newObj
}
深拷贝:开辟一个新的栈,两个对象的属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
* @param {Map} map 用于存储循环引用对象的地址
*/
function deepClone(obj = {}, map = new Map()) {
if (obj === null) return obj // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== 'object') return obj
if (map.get(obj)) {
return map.get(obj);
}
let result = {}; // 初始化返回结果
if (
obj instanceof Array ||
// 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此
Object.prototype.toString(obj) === "[object Array]"
) {
result = [];
}
// 防止循环引用
map.set(obj, result);
for (const key in obj) {
// 保证 key 不是原型属性
if (obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key], map);
}
}
return result;
}
22. 箭头函数的this指向
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this
,它所谓的this
是捕获其所在上下⽂的 this
值,作为⾃⼰的 this
值,并且由于没有属于⾃⼰的this
,所以是不会被new
调⽤的,这个所谓的this
也不会被改变
23. JavaScript
中内存 泄漏的几种情况
内存泄漏
一般是指系统进程不再用到的内存,没有及时释放,造成内存资源浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
-
全局变量 在局部作用域中,函数执行完毕后,变量就没有存在的必要了,垃圾回收机制很快的做出判断并回收;但是对于全局变量,很难判断什么时候不用这些变量,无法正常回收。所以尽量少使用全局变量;使用严格模式,在js文件头部或者函数的顶部加上
use strict
-
闭包引起的内存泄露 闭包可以读取函数内部的变量,然后让这些变量始终保存在内容中,如果在使用结束后没有将局部变量清楚,就可能导致内存泄漏。所以将事件处理函数定义在外部,解除闭包。
-
被遗忘的定时器 定时器
setInterval
、setTimeout
不再需要使用时,且没有被清除,导致定时器的回调函数及其内部依赖的变量都不能被回收,造成内存泄漏。所以当不需要定时器的时候,调用clearInterval
、clearTimeout
手动清除 -
事件监听 垃圾回收机制不好判断事件是否需要被解除,导致
callback
不能被释放,此时需要手动解除绑定。及时使用removeEventListener
移除事件监听 -
元素引用没有清除 移除元素后,手动设置元素的引用为null
-
console 传递给
console.log
的对象是不能被垃圾回收,可能会存在内存泄漏。所以清除不必要的console
24. 扩展运算符的作用及使用场景
(1)对象扩展运算符
对象的扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象中。如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。扩展运算符对对象实例的拷贝属于浅拷贝。
let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }
let bas = {...bar, ...{a:2, b: 4}}; // {a: 2, b: 4}
(2)数组扩展运算符
数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。
console.log(...[1, 2, 3]); // 1 2 3
console.log(...[1, [2, 3, 4], 5]); // 1 [2, 3, 4] 5
function add(x, y) {
return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3
const arr1 = [1, 2];
const arr2 = [...arr1]; // [1, 2]
const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five']; // ["one", "two", "three", "four", "five"]
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]
[...'hello'] // [ "h", "e", "l", "l", "o" ]
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Math.max(...numbers); // 9
25. 原型与原型链
- 所有的对象本质上都是通过
new 函数
创建的,包括对象字面量的形式定义对象- 所有的函数本质上都是通过
new Function
创建的,包括Object
、Array
等- 所有的函数都是对象
prototype
每个函数都有一个属性prototype
,它就是原型,默认情况下它是一个普通Object
对象,这个对象是调用该构造函数所创建的实例的原型
constructor
JavaScript
同样存在由原型指向构造函数的属性:constructor
,即Func.prototype,constructor --> Func
__proto__
所有对象(除了null)都具有一个__proto__
属性,该属性指向该对象的原型
function User() {}
var u1 = new User();
// u1.__proto__ -> User.prototype
console.log(u1.__proto__ === User.prototype) // true
var u2 = new User();
console.log(u1.__proto__ === u2.__proto__) // true
原型链:实例对象在查找属性时,如果查找不到,就会沿着__proto__
去与对象关联的原型上查找,如果还查找不到,就去找原型的原型,直至查到最顶层
原型链指向
所有函数(包括Function)的
__proto__
指向Function.prototype
自定义对象实例的
__proto__
指向构造函数的原型函数的
prototype
的__proto__
指向Object.prototype
Object.prototype.__proto__
-->null
原型修改、重写
function Person(name) {
this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // false
26. 对闭包的理解
闭包 是一个函数以及其捆绑的周边环境状态(词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
闭包的应用场景
- 匿名自执行函数
- 结果缓存
- 封装
- 实现类和继承
闭包的缺点 因为闭包的作用域链会引用包含它的函数的活动对象,导致这些活动对象不会被销毁,因此会占用更多的内存。
27. 防抖与节流的区别
防抖
:多次触发事件,事件处理函数只执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发,准备执行事件函数前,会等待一定的时间,在这个等待时间内,如果没有再次被触发,那么就执行,如果又触发了,那就本次作废,重置等待时间,直到最终能执行。
主要应用场景:搜索框搜索输入,用户最后一次输入完,再发送请求;手机号、邮箱验证输入检测
/*** 防抖函数 n 秒后再执行该事件,若在 n 秒内被重复触发,则重新计时
* @param func 要被防抖的函数
* @param wait 规定的时间
*/
function debounce(func, wait) {
let timeout = null;
return function () {
let context = this; // 保存this指向
let args = arguments; // 拿到event对象
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function () {
func.apply(context, args)
}, wait)
}
}
节流
:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。
主要应用场景:高频点击、表单重复提交等。
/*** 节流函数 n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
* @param fn 要被节流的函数
* @param wait 规定的时间
*/
function throttled(fn, wait) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, wait);
}
}
}
28. 对事件循环(Event Loop)的理解
Event Loop
即事件循环,是浏览器或Node
解决单线程运行时不会阻塞的一种机制。
JavaScript
的确是一门单线程语言,但是浏览器UI
是多线程的,异步任务借助浏览器的线程和JavaScript
的执行机制实现。 例如,setTimeout
就借助浏览器定时器触发线程的计时功能来实现。
浏览器线程
-
GUI
渲染线程- 绘制页面,解析HTML、CSS,构建DOM树等
- 页面的重绘和重排
- 与JS引擎互斥(JS引擎阻塞页面刷新)
-
JS
引擎线程- js脚本代码执行
- 负责执行准备好的事件,例如定时器计时结束或异步请求成功且正确返回
- 与GUI渲染线程互斥
-
事件触发线程
- 当对应的事件满足触发条件,将事件添加到js的任务队列末尾
- 多个事件加入任务队列需要排队等待
-
定时器触发线程
- 负责执行异步的定时器类事件:setTimeout、setInterval等
- 浏览器定时计时由该线程完成,计时完毕后将事件添加至任务队列队尾
-
HTTP
请求线程- 负责异步请求
- 当监听到异步请求状态变更时,如果存在回调函数,该线程会将回调函数加入到任务队列队尾
同步与异步执行顺序
JavaScript
将任务分为同步任务和异步任务,同步任务进入主线中中,异步任务首先到Event Table
进行回调函数注册。- 当异步任务的触发条件满足,将回调函数从
Event Table
压入Event Queue
中。 - 主线程里面的同步任务执行完毕,系统会去
Event Queue
中读取异步的回调函数。 - 只要主线程空了,就会去
Event Queue
读取回调函数,这个过程被称为Event Loop
。
宏任务和微任务
JavaScript广义上将任务分为同步任务和异步任务,还对异步任务更精细的划分为微任务和宏任务
宏任务包括:script
脚本的执行、setTimeout
、setInterval
、setImmediate
、AJAX
、I/O
、history traversal(h5当中的历史操作)
、UI渲染
等
微任务:Promise.then
、Object.observe
、process.nextTick(nodejs中的一个异步操作)
、MutationObserver(h5里面增加的,用来监听DOM节点的变化)
等
Event Loop
执行过程
-
代码开始执行,创建一个全局调用栈,
script
作为宏任务执行 -
执行过程中同步任务立即执行,异步任务根据异步任务类型分别注册微任务队列和宏任务队列
-
同步任务执行完毕,查看微任务队列
- 若存在微任务,将微任务队列全部执行(包括执行微任务过程中产生新微任务)
- 若无微任务,查看宏任务队列,执行第一个宏任务,宏任务执行完毕,查看微任务队列,重复上述操作,直至宏任务队列为空
29. Proxy
和Object.defineProperty
的区别
-
代理原理:
Object.defineProperty
的原理是通过将数据属性转变为存取器属性的方式实现的属性读写代理。而Proxy
则是因为这个内置的Proxy
对象内部有一套监听机制,在传入handler
对象作为参数构造代理对象后,一旦代理对象的某个操作触发,就会进入handler
中对应注册的处理函数,此时我们就可以有选择的使用Reflect
将操作转发被代理对象上。 -
代理局限性:
Object.defineProperty
始终还是局限于属性层面的读写代理,对于对象层面以及属性的其它操作代理它都无法实现。鉴于此,由于数组对象push
、pop
等方法的存在,它对于数组元素的读写代理实现的并不完全。而使用Proxy
则可以很方便的监视数组操作。 -
自我代理:
Object.defineProperty
方式可以代理到自身(代理之后使用对象本身即可),也可以代理到别的对象身上(代理之后需要使用代理对象)。Proxy
方式只能代理到Proxy
实例对象上。这一点在其它说法中是Proxy
对象不需要侵入对象就可以实现代理,实际上Object.defineProperty
方式也可以不侵入。
30. Ajax
、Axios
、Fetch
的区别
Ajax
Ajax
是一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax
可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页如果需要更新内容,必须重载整个网页页面。
缺点
- 本身是针对MVC编程,不符合前端MVVM的浪潮
- 基于原生XHR开发,XHR本身的框架不清晰
- 不符合关注分离的原则
- 配置和调用方式非常混乱,而且基于事件的异步模型不友好
Axios
Axios
是一种基于Promise封装的HTTP客户端
特点
- 浏览器端发起
XMLHttpRequests
请求node
端发起http请求- 支持Promise API
- 监听请求和返回
- 对请求和 返回进行转化
- 取消请求
- 自动转换json数据
- 客户端支持抵御
XSRF
攻击
Fetch
Fetch
号称是Ajax
的替代品,是在ES6出现的,使用了ES6中的Promise
对象。Fetch
是基于Promise
设计的;代码结构比Ajax
简单。Fetch
不是Ajax
的进一步封装,而是原生JavaScript
,没有使用XMLHttpRequest
对象。
优点
- 语法简洁,更加语义化
- 基于标准
Promise
实现,支持async/await
- 更加底层,提供的API丰富(
request
,response
)- 脱离了XHR,是ES规范里新的实现方式
缺点
Fetch
只对网络请求报错,对400
、500
都当成功的请求,服务器返回400
、500
错误码时并不会reject
,只有网络错误这些导致请求不能完成时,fetch
才会被reject
Fetch
默认不会带cookie
,需要添加配置项:fetch(url, { credentials: "include" })
Fetch
不支持abort
,不支持超时控制,使用setTimeout
及Promise.reject
实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费Fetch
没有办法原生监测请求的进度,而XHR可以
31. 常见DOM操作
DOM节点获取
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节点创建
// 首先获取父节点
var container = document.getElementById('container')
// 创建新节点
var targetSpan = document.createElement('span')
// 设置 span 节点的内容
targetSpan.innerHTML = 'hello world'
// 把新创建的元素塞进父节点里去
container.appendChild(targetSpan)
DOM节点删除
// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = document.getElementById('title')
// 删除目标元素
container.removeChild(targetNode)
DOM节点修改
// 获取父元素
var container = document.getElementById('container')
// 获取两个需要被交换的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交换两个元素,把 content 置于 title 前面
container.insertBefore(content, title)