JS详解面试题46题
1、对原型链的理解
JavaScript中的每个对象都有一个内部的[[Prototype]]属性,也称为原型(prototype),它指向另一个对象或者null。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着它的原型链一层层向上查找,直到找到该属性或方法或者原型链到达顶端(即Object.prototype,它是所有对象的祖先)为止。
通过原型链,我们可以实现继承、属性的共享和方法的复用。当我们创建一个对象时,可以通过指定它的原型来继承原型上的属性和方法。在查找属性或方法时,如果对象本身没有这个属性或方法,就可以在其原型上查找。这样可以避免在每个对象上都复制一份相同的属性和方法,从而节省内存。
需要注意的是,如果在原型链上的某个属性被修改了,那么所有继承自该原型的对象都会受到影响。因此,应该谨慎地修改原型上的属性和方法。另外,通过对象的constructor属性,可以访问到其构造函数,从而可以判断一个对象的类型。
2、map 和 Object 的区别
关键词:键值区别、长度大小获取、原型、顺序、循环
1、Object的键只能是字符串或Symbol,但是Map的键可以是任意值
2、Map的长度大小可以通过size来获取,但是Object则需要通过Object.keys(obj).length
3、Object的键值可能会和原型上的属性重名,我们可以使用Object.create(null)来设置没有原型的对象
4、Object的key的顺序是先判断数字开头的key,在判断字符串,而Map的顺序是在插入时决定的
5、Map结构的数据可以使用for of来循环,而Object则需要手动遍历,用for in
6、Map在频繁增删键值对的场景下表现更好。
综上所诉:如果需要存储不同的键值并且保持插入顺序的话用Map会更加灵活
3、this 指向
关键词:函数执行上下文、箭头函数、new
规则:箭头函数>new>显式调用>隐式调用>默认绑定
解释:this是javascript中的一个关键字,多数情况下指向的是调用它的对象
首先,this应该指向的是一个对象(函数执行上下文对象),其次这个对象指向的是调用它它的对象,如果调用它的不是一个对象或者对象不存在,那么它指向的就是全局对象(严格模式下为undefined)
其实,this在函数被调用时就确定了,它的指向就是函数被调用的地方,而不是它声明的地方(除箭头函数外)。当函数被调用时,会创建一个执行上下文,它包含了函数在哪里被调用(调用栈)、函数调用的方式、以及函数的参数等信息,this就是一个记录它的一个属性,在函数执行的过程中会被用到
1.、默认绑定
函数的浏览器在全局环境中直接使用代表的是全局对象Window,在node环境下则是Global,如果是严格模式下,则是undefined
2、隐式绑定
理解:谁调用就指向谁
3、显式绑定
**apply、call、bind都是可以改变函数指向,**但是 call 和 apply 是直接进行函数调用 ,不会执行函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的 this 指向,需要我们手动调用。 call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组
const target = {}
fn.call(target, 'arg1', 'arg2')
fn.apply(target, ['arg1', 'arg2'])
fn.bind(target, 'arg1', 'arg2')()
如果传入了null或者undefined作为this指向,则this会使用默认绑定规则
并且使用多次bind绑定,永远取第一次this的指向
4、new绑定
函数作为构造函数使用 new 调用时, this 绑定的是新创建的构造函数的实例
5、特殊this指向
1.箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this
2.数组方法:例如forEach、find、findIndex、map、some、every都可以改变this指向(第二个参数)
3.立即执行函数:就是默认的全局对象 window
4.setTimeout和setInterval:其实,延时效果(setTimeout)和定时效果(setInterval)都是在全局作用域下实现的。无论是 setTimeout 还是 setInterval 里传入的函数,都会首先被交到全局对象手上。因此,函数中 this 的值,会被自动指向 window
6、不在函数中的场景,可分为浏览器的 标签里,或 Node.js 的模块文件里。
1) 在 标签里,this 指向 Window。
2) 在 Node.js 的模块文件里,this 指向 Module 的默认导出对象,也就是 module.exports。
4、闭包的作用、原理和使用场景
一个函数和对其周围状态的引用捆绑在了一起(或者说函数被引用包围),这样的组合就是闭包。也就是说闭包就是一个让你可以访问外层函数的作用域。通俗来说,闭包其实就是一个可以访问其他函数内部变量的一个函数,即定义在函数内部的函数,或者说闭包就是一个内嵌函数。
通常来说,函数内部的变量是无法在外部进行访问的(即全局变量和局部的变量的区别),因此使用闭包的作用实现了,能在外部访问某个函数内部的变量的功能,让这些变量的值始终可以保存在内存当中。
从直观上讲,闭包这个概念为JavaScript中访问函数内变量提供了途径和便利。这样做的好处有很多,比如闭包实现缓存等。
2、作用域链的概念
当访问一个变量时,代码编辑器会首先在当前作用域去查找,如果没还找到,就会继续往父级作用域去查找,直到找到该变量或者不存在父级作用域内,这样的链路操作就是作用域链。
3、闭包的本质是什么?
当前环境内存在对于父级作用域的引用
4、闭包的使用场景
- 在定时器、事件监听、Ajax请求、web workers、任何异步中,只要使用了回调函数,实际上就是使用闭包,防抖和节流,以及vue底层都使用了
// 定时器
setTimeout(function handler(){
console.log('1');
},1000);
// 事件监听
document.getElementById(app).addEventListener('click', () => {
console.log('Event Listener');
});
- 作为函数参数的传递的形式
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这是闭包
fn();
}
foo(); // 输出2,而不是1
- 立即执行函数:创建了闭包,保存了全局作用域和当前作用域,因此可以输出全局的变量。
function memorize(fn) {
const cache = {}
function foo(args) {
const key = JSON.stringify(args)
let result = cache[key]
if (!result) {
cache[key] = args
result = fn(args)
}
return { cache, result }
}
foo.cache = cache
return foo
}
function add(a) {
return a + 1
}
const added = memorize(add)
console.log("", added(1))
console.log("", added(2))
console.log("", added(3))
console.log("", added(4))
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(2) // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }
5、常见的闭包的循环输出的问题
for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i)
}, 0)
}
输出5个6
解决办法:
1、利用立即执行函数
for(var i = 1; i <= 5; i ++){
(function(j){
setTimeout(function() {
console.log(j)
}, 0)
})(i)
}
2、使用es6的let
3、使用定时器的第三个参数
for(var i=1;i<=5;i++){
setTimeout(function(j) {
console.log(j)
}, 0, i)
}
定时器的第三个参数,这些参数会作为回调函数的附加参数存在
5、正向代理和反向代理的区别
正向代理和反向代理是两种不同类型的代理服务器,它们在网络通信中扮演不同的角色。
- 正向代理(Forward Proxy):
- 正向代理作为客户端的代理,代表客户端向其他服务器发送请求。客户端需要通过正向代理来访问其他服务器,因为直接访问会受到限制或阻止。
- 举个例子,当你在公司内部网络中访问互联网时,你可能需要通过公司的正向代理服务器来访问外部网站,因为公司的网络设置了防火墙或者其他安全限制。
- 反向代理(Reverse Proxy):
- 反向代理作为服务器的代理,代表服务器接收客户端的请求并将请求转发到内部的服务器。客户端不知道自己实际正在与哪个服务器通信,因为所有的请求都是发送到反向代理服务器。
- 举个例子,当你访问一个网站时,你实际上在与反向代理服务器通信,它会将你的请求转发到后端的多个服务器上,然后将结果返回给你。
总的来说,正向代理是代表客户端发出请求,而反向代理是代表服务器接收请求。它们的主要区别在于代理的角色不同,以及它们在网络通信中的位置和功能不同。
6、箭头函数和普通函数的区别
区别:
1、箭头函数比普通函数更加简洁:
箭头函数返回值只有一句,直接可以省略大括号。如果不需要返回值直接加上void就可以了
2、箭头函数没有自己的this
箭头函数不会创建自己的this,所以它没有this,它只会继承自己作用域的上一层,所以箭头函数在创建的时候this指向就已经定义好了
3、箭头的函数的this指向永远不会被改变
4、call、apply、bind等方法不会改变箭头函数this的指向
5、箭头函数不能作为构造函数使用:因为箭头函数没有自己的this,所以会报错
6、箭头函数没有自己的arguments:箭头函数没有自己的argumnets对象,在箭头函数中访问的实际上获得的是它外层函数的arguments值
7、箭头函数没有prototype
8、箭头函数不能作为Genrator函数,不能使用yeild关键字
7、ES6新特性
1、let、const
2、模版字符串
3、解构赋值
4、函数设置默认值
5、...运算符
6、箭头函数
7、for of
- for of 不能遍历对象,可以遍历数组
- for in 遍历得到的是下标或者是对象的key
- 可迭代对象可以使用for of

- for of 不同与forEach,前者可以使用break、continue、return配合使用,也就是可以随时跳出循环
8、class类,原型链的语法糖的表现形式
9、导入导出 export default 、import
10、Promise
11、async 和 await
12、Symbol 新的基本类型
13、Set集合
8、promise.all和promise.allSettled的区别
他们都是promise的静态方法,用与处理多个promise实例。
区别:
1、返回值不同:Promise.all的返回值是一个对象,当所有的Promise实例都成功时,返回的Promise对象的状态都是fulfilled,其中包含了应该所有Promise实例结果的数组;当有任何一个Promise实例失败的时候,返回的Promise对象的状态是rejected,并返回一个被拒绝的Promise的结果;而Promise.allSettled的返回值是一个Promise对象,当所有的Promise实例执行完成后(不管成功还是失败),返回的Promise对象的状态为fulfilled,其结果包含所有Promise实例执行结果的数组,数组的每一个元素都是一个对象,包含Promise实例的状态和结果信息
2、处理方式不同:Promise.all在所有实例resolved或者有一个实例rejected,就会立即终止Promise实例的执行,即使还有Promise没有完成;而Promise.allSettled会等到所有Promise完成,无论是成功还是失败。
status:‘’,value或者status,reason
使用:Promise.all可以使用在例如两个接口不互相依赖,可以同时执行请求,减少请求的时间
9、subString、subStr区别
两个方法都用于截取字符串,但是用法不同:
- substring 方法:
- substring 方法用于从字符串中提取子字符串,并返回提取的子字符串。
- 它接收两个参数,分别表示要提取的子字符串的起始位置和结束位置(不包括结束位置的字符)。
- 如果省略第二个参数,则会提取从起始位置到字符串末尾的所有字符。
- 如果第一个参数大于第二个参数,则 substring 会自动交换这两个参数的位置
const str = "Hello, World!";
console.log(str.substring(1, 4)); // "ell"
console.log(str.substring(4, 1)); // "ell",自动交换参数位置
console.log(str.substring(7)); // "World!",省略第二个参数
- substr 方法:
- substr 方法也用于从字符串中提取子字符串,并返回提取的子字符串。
- 它接收两个参数,第一个参数表示要提取的子字符串的起始位置,第二个参数表示要提取的字符数。
- 如果第二个参数为负数,则表示从字符串末尾开始计算字符数。
- 示例:
javascriptconst str = "Hello, World!";
console.log(str.substr(1, 4)); // "ello"
console.log(str.substr(7)); // "World!",省略第二个参数
console.log(str.substr(-6, 5)); // "World",负数表示从末尾开始计算字符数
10、symbol作用和使用场景
解释:Symbol是ES6中引入的新 数据类型,表示的是一个唯一得常量,通过Symbol函数来创建对应的数据类型,可以在创建的时候添加变量描述,该变量会被强制转换为字符串
用法:常量值和对象属性
- 避免常量值重复
- 避免对象属性覆盖:给函数的参数赋值,如果参数是一个对象,且包含相同属性,就可以利用Symbol来赋值和读取
11、JS脚本异步加载
1.当script上没有defer或者async的时候,浏览器在执行脚本的时候会阻塞后续文档的加载。当存在defer或async的时候都是异步加载,它们不会阻塞页面的解析:
区别:
1、执行顺序:多个async属性不会保证执行的顺序,多个defer属性会保证执行的顺序
2、脚本是否并行执行:async会异步下载解析脚本(即加载后续文档元素的过程中和js脚本的加载是并行进行);而defer表示延迟加载,脚本会先不执行,而是等到文档解析完成后再去执行。
2.JavaScript脚本延迟加载的方式有哪些?
延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。
一般有以下几种方式:
- **defer 属性:**给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
- **async 属性:**给 js 脚本添加 async 属性,这个属性 会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
- **动态创建 DOM 方式:**动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
- **使用 setTimeout 延迟方法:**设置一个定时器来延迟加载js脚本文件
- 让 JS 最后加载:将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
12、typeof和intanceof区别
关键词:递归、proto、 prototype 例如: L instanceof R instanceof运算时,会递归L的__proto__原型链,查看是否存在右侧的prototype原型。
function myInstanceOf(left,right){
if (typeof left !== 'object' || left === null || typeof right !== 'function') {
return false;
};
let proto = Object.getPrototypeOf(left) // 获取左边对象的原型
let prototype = right.prototype
// 判断构造函数的 prototype 对象是否在对象的原型链上
while(true){
if(!proto) return false
if(proto===prototype) return true
proto = Object.getPrototypeOf(proto)
}
}
typeof与instanceof 都是判断数据类型的方法,区别如下:
● typeof会返回一个运算数的基本类型,instanceof 返回的是布尔值
● instanceof 可以准确判断引用数据类型,但是不能正确判断原始数据类型
● typeof虽然可以判断原始数据类型(null 除外),但是无法判断引用数据类型(function 除外)
**扩展:**typeof的原理
typeof原理: 不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位存储其类型信息。
- 000: 对象
- 010: 浮点数
- 100:字符串
- 110: 布尔
- 1: 整数
typeof null 为"object", 原因是因为 不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位都为0的话会被判断为Object类型,null的二进制表示全为0,自然前三位也是0,所以执行typeof时会返回"object"。 一个不恰当的例子,假设所有的Javascript对象都是16位的,也就是有16个0或1组成的序列,猜 想如下:
Array: 1000100010001000
null: 0000000000000000
typeof [] // "object"
typeof null // "object"
因为Array和null的前三位都是000。为什么Array的前三位不是100?因为二进制中的“前”一般代表低位, 比如二进制00000011对应十进制数是3,它的前三位是011。
还有一种情况
function foo() {};
typeof foo; // 'function'
这样看来,function 也是JavaScript的一个内置类型。然而查阅规范,就会知道,它实际上是 object 的一个"子类型"。具体来说,函数是“可调用对象”,它有一个内部属性[[call]],该属性使其可以被调用。typeof 可以用来区分函数其他对象。
typeof可以对基本数据类型进行类型判断,例如number, string, object, boolean, function, undefined, symbol,但是在判断object类型的时候,而不能细致的具体到哪一种object,如果要想具体到哪一种object,可
推荐使用:Object.prototype.toString.call()
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('hi') // "[object String]"
Object.prototype.toString.call({a:'hi'}) // "[object Object]"
Object.prototype.toString.call([1,'a']) // "[object Array]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(() => {}) // "[object Function]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
13、forEach、map是否可以break
map 和forEach无法使用return 或break跳出循环;但是可以抛出throw new Error(),通过try catch去捕获这个错误终止循环