😊JS面试八股文(二)
- 11.JS是如何实现继承的?
- 12.JS的设计原理是什么?
- 13.JS中关于this指向的问题
- 14.script标签里的async和defer有什么区别?
- 15.setTimeout最小执行时间是多少?
- 16.ES6和ES5有什么区别?
- 17.ES6的新特性有哪些?
- 18.call、apply、bind三者有什么区别?
- 19.用递归的时候有没有遇到什么问题?
- 20.如何实现一个深拷贝?
😊各位小伙伴们,本专栏新文章出炉了!!!
11.JS是如何实现继承的?
原型链继承
让一个构造函数的原型是另一个类型的实例,那么这个构造函数new出来的实例就具有该实例的属性。
缺点:对象实例共享所有继承的属性和方法,但是无法向父类构造函数传参。
<script>function Person(name){this.name = name;}Person.prototype.getName = function(){console.log("我的名字是"+name)}function Student(name,age){Person.call(this,name)this.age = age;}//设置Student的原型为Person的一个实例Student.prototype = Object.create(Person.prototype);Student.prototype.constructor = Student;Student.prototype.getAge = function(){console.log('我的年龄是'+this.age)}let student = new Student("张三",18)student.getName();student.getAge();
</script>
构造函数继承
通过在子类构造函数内部调用父类构造函数,并传入this
作为父类构造函数的上下文,可以实现属性继承。使用apply()
或call()
方法将父对象的构造函数绑定在子对象上。并且这种方式解决了原型链实现继承不能传参的问题和父类的原型共享的问题。
缺点:每次实例化都调用父类构造函数。无法继承父类的方法,构造函数继承只能继承父类的属性,不能继承方法。属性共享问题,如果在父类构造器中定义了引用类型的属性(如数组,对象等)这些属性在每次子类实例化时都会共享同一个引用。
<script>function Parent(){this.name = "张三"}function Child(){Parent.call(this); //在Child构造函数内部调用Parent构造函数this.age = 18}let child = new Child();console.log(child.name); //此时输出“张三”
</script>
组合继承(组合原型链继承和构造函数继承)
将原型链继承和构造函数继承结合到一块,使用原型链实现对原型属性和方法的继承,而又通过借助构造函数来实现对实例属性的继承,既通过原型上定义方法实现函数复用,又能够保证每个实例都有自己的属性。
缺点:无论在什么情况下,都会调用两次父类构造函数。
<script>function Parent(name){console.log("我被调用了")this.name = name}//原型链Parent.prototype.getName = function(){return this.name}function Child(name,age){Parent.call(this,name); //调用父类构造函数,借用构造函数继承this.age = age}//原型链继承Child.prototype = new Parent();Child.prototype.getAge = function(){console.log("年龄是",this.age)}let child = new Child("张三",18);console.log(child.getName());child.getAge();
</script>
类继承(ES6)
ES6引入了class
和extends
关键字实现继承,子类的构造方法中必须调用super()
方法,且只有在调用了super()
之后才能使用this
。
<script>class Parent{constructor(name){this.name = name;this.array = [1,2,3]}getName(){return this.name;}}class Child extends Parent{constructor(name,age){super(name) //调用父类构造函数this.age = age;}getAge(){return this.age}}let child = new Child("张三",18)console.log(child.getName())console.log(child.getAge())
</script>
通过类继承,可以实现属性和方法的继承,并且通过重写方法来拓展或改变父类的行为。
12.JS的设计原理是什么?
JS引擎
JavaScript引擎是负责执行JavaScript代码的软件组件。它将JavaScript代码转换为机器码,使得浏览器或服务器可以执行这些代码。
运行上下文
在JavaScript中,运行上下文是指在执行JavaScript代码时,引擎用来存储相关信息的数据结构。运行上下文对于理解JavaScript的执行机制非常重要,因为它涉及到变量、函数声明、作用域链、this
值等关键概念。
运行上下文主要分为:全局上下文和函数上下文。
调用栈
用于记录函数调用的历史,帮助解释器或虚拟机追踪当前正在执行的函数以及他们的调用顺序。
作用:
- 记录函数调用
- 每当一个函数被调用时,调用栈就会记录一条新的条目(称为帧或堆栈帧)、表示当前函数正在执行。
- 每个条目包含了当前函数的运行上下文,包括局部变量、参数、作用域链等信息
- 跟踪执行顺序:
- 调用栈按照先进后出的工作原则,即最后进入调用栈的函数最先被弹出。
- 当一个函数执行完毕之后,它对应的条目会从调用栈中移除(弹出)。
- 异常处理:
- 当函数执行过程中出现异常时,可以通过调用栈来追踪异常发生的上下文。
- 异常处理机制通常依赖于调用栈来决定异常应该传递给哪个调用者
- 递归调用
- 咋递归调用中,每次递归调用都会在调用栈中添加一个新的条目。
- 如果递归深度过大,可能会导致调用栈溢出。
事件循环
事件循环负责协调JavaScript代码的执行与浏览器(或其他运行环境)的其他活动,如用户输入、网络请求、定时器等。
回调
回调是JavaScript中处理异步操作的一种常见模式。回调函数本质上就是一个作为参数传递给另一个函数的函数,当外部函数完成某些操作后,他会调用回调函数来通知操作的结果或执行下一个步骤。
13.JS中关于this指向的问题
全局上下文中的this
- 在全局作用域下,
this
指向的是全局的window
对象。
全局作用域或普通函数中的this
- 指向全局对象
window
。
方法调用中的this
- 当一个函数作为某个对象的方法被调用时,
this
指向该对象。
构造函数调用中的this
- 当使用
new
关键字调用构造函数时,构造函数内部的this
指向新创建的对象。
事件处理器中的this
- 在事件处理器中,
this
指向触发事件的DOM
元素。
箭头函数中的this
- 箭头函数没有自己的
this
值,它捕获外层作用域的this
值。这意味着箭头函数内部的this
与定义时的上下文有关,而不是调用时的上下文。
bind
、call
、apply
方法中的this
bind
返回一个新函数,当调用时,this
的值被固定位提供的值。
<script>const obj = {value:20,logValue:function(){console.log(this.value)}}const boundLogValue = obj.logValue.bind({value:22})boundLogValue(); //输出为22
</script>
call
立即调用函数,并指定this
的值
<script>function logValue(){console.log(this.value);}logValue.call({value:22}) //输出:22,1,2
</script>
apply
立即调用函数,并指定this
的值,参数以数组形式传递
<script>function logValue(a,b){console.log(this.value,a,b);}logValue.apply({value:22},[1,2]) //输出:22,1,2
</script>
匿名函数中的this
- 永远指向了
window
,匿名函数的执行环境具有全局性,因此this
指向window
。
14.script标签里的async和defer有什么区别?
async
属性
- 异步加载
- 当
<script>
标签里包含了async
属性时,脚本会被异步加载,脚本的加载不会阻塞页面的渲染。浏览器会在下载其他资源的时候同时加载脚本,提高页面的加载速度。
- 执行时机
async
在加载完之后立即执行,但具体的执行时机不确定,async
的执行顺序不受控制。- 加载和渲染后序页面元素的过程将和
Script
的加载和执行并行进行(异步)
defer
属性
- 异步加载
- 当
<Script>
标签内包含defer
属性时,脚本也会被异步加载,同样不会阻塞页面的渲染。
- 执行时机
defer
会在DOM
完全解析完成后执行,defer
执行的顺序是按照脚本在HTML
文档中出现的脚本先执行。
15.setTimeout最小执行时间是多少?
setTimeout
的最小执行时间根据不同的规范和浏览器实现有所不同,但是根据HTML5
规定setTimeout
和setInterval
的最小执行实现分别为20ms
和10ms
。
然而,实际上由于浏览器实现的不同,在某些情况下,setTimeout
的最小执行时间是4ms
。
在实际开发中,浏览器通常会对setTimeout
的延迟时间有一个最小值限制,例如:
- 如果设置的延迟时间小于
4ms
,则实际延迟时间会被调整为至少4ms
- 如果设置的延迟时间小于
10ms
,则实际延迟时间会被调整为至少10ms
。
16.ES6和ES5有什么区别?
变量声明
- ES5使用
var
关键字声明变量,但存在变量提升和函数作用域的问题。 - ES6引入了
let
和const
关键字,let
声明的变量具有块作用域,const
声明常量,不可重新处置,同样具有块作用域。
模块化支持
- ES5没有原生的模块支持,通常使用立即执行函数表达式来模拟模块。
- ES6引入了
import
和export
关键字,支持模块化编程。
类
- ES5只能使用构造函数和原型链来实现类的功能。
- ES6引入了类语法,使得面向对象编程更加直观。
箭头函数
- ES5使用传统的函数表达式。
- ES6引入了箭头函数,语法更加简洁,并且
this
的绑定更加自然。
解构赋值
- 解构赋值是ES6新增的特性,支持对象和数组的解构赋值,使得从复杂数据结构中提取数据更加方便。
<script>const [a, b] = [1, 2];console.log(a, b) //输出1,2const { name, age } = { name: '张三', age: 18 };console.log(name, age); //输出 张三,18
</script>
默认参数
- ES5使用条件语句或默认值来设置函数参数的默认值。
- ES6支持直接在函数参数中设置默认值。
<script>//ES5function greet(name){name = name || '张三'console.log("我是",name)}greet(); //输出 我是张三function greet1(name='张三'){console.log("我是",name);}greet1(); //输出 我是张三
</script>
模板字符串
- ES5使用字符串拼接或字符串字面量
- ES6引入了模板字符串,支持多行文本和变量插值。
新增的数据结构
- ES6中引入了
Set
和Map
数据结构,提供了更强大的集合操作功能。
符号(Symbols)
- ES6中引入了
Symbol
类型,用于创建唯一的标识符。
Promises
- ES5中使用回调函数处理异步操作
- ES6中引入了
Promise
对象,提供了一种更优雅的方式处理异步操作。
17.ES6的新特性有哪些?
- 变量声明
let
声明具有块级作用域的变量const
声明常量,即不可重新赋值的变量,同样具有块级作用域。
- 箭头函数
- 箭头函数提供了一种更简洁的函数定义方式,并且
this
的值绑定在定义时的环境,而不是执行时的环境。
- 模板字符串
- 模板字符串允许方便地插入变量,并支持多行文本。
- 解构赋值
- 解构赋值可以从数组或对象中便捷地提取值并赋给变量。
- 扩展运算符和剩余参数
- 扩展运算符
(...)
用于展开数组或对象中的元素。 - 剩余参数
(...)
用于收集传入函数的多余参数。
- 类
- 类提供了一个更接近传统面向对象编程的语言特性,尽管底层仍然是基于原型链的继承。
- 模块化支持
- ES6引入了模块的概念,允许使用
import
和export
关键字来导入和导出模块。
- 异步编程
Promise
提供了一种处理异步操作的模式,改善了回调地狱的问题。async/await
虽然不是ES6的一部分,但他们建立在Promise
的基础上,提供了更简洁的异步代码编写方式。
- 新的数据结构
Map
:类似对象的数据结构,key
可以是任意类型。Set
:存储唯一值的集合。WeakMap
和WeakSet
:键或值可以是弱引用,允许垃圾回收。
- 符号
Symbol
类型用于创建唯一的键名,防止属性名冲突。
- 迭代器和生成器
- 迭代器协议能使对象可以被迭代。
- 生成器函数可以暂停执行,并在后续调用中恢复执行。
- 数组和对象的新方法
- 数组:如
includes
、find
、findIndex
、entries
、keys
、values
等。 - 对象:如
Object.assign(用于浅拷贝对象)
18.call、apply、bind三者有什么区别?
call
、apply
、bind
都是用来控制函数执行时this
值的方法,但是他们在使用方式和返回值上有所不同。
call
方法改变this
值后,立即执行该函数。接受参数的方式为参数列表apply
改变this
值后,立即执行该函数,但它接受参数的方式不同,传递的参数是一个数组。bind
方法传参后不会立即执行,会返回一个新的函数,这个函数可以在后续的某个时刻被调用。
区别总结
- 立即执行 VS 返回新函数
call
和apply
都是立即执行函数。bind
返回一个新的函数,这个函数可以在未来的某个时刻被调用。
- 参数传递方式
call
接受一系列参数作为函数的参数。apply
接受一个数组或类数组对象作为参数列表。
- 用途
call
和apply
通常用于在调用函数时临时更改this
的值。bind
通常用于创建一个新的函数实例,这个新函数可以绑定到特定的this
值,并且可以预设部分参数。
19.用递归的时候有没有遇到什么问题?
- 堆栈溢出
递归函数如果层数过深,会导致调用栈过大,最终导致浏览器或运行时环境报“堆栈溢出”错误。因为每次函数调用都会在调用栈上占用一定的空间,如果出现递归层数过多,就很可能会导致栈空间耗尽。
- 终止条件不明确
递归函数如果没用明确的终止条件或终止条件设置不当,可能会导致无限递归。
20.如何实现一个深拷贝?
深拷贝常用于引用类型,使用深拷贝就意味着创建了一个对象或数组的副本,其中对象的所有属性都被复制,包括嵌套的对象和数组。并且,在使用深拷贝后,原始对象和副本之间没有任何引用关系,修改副本并不会影响到原始数据。
- 使用
JSON
方法
利用JSON.parse()
和JSON.stringify()
方法可以简单的实现深拷贝
<script>function deepCopy(obj) {return JSON.parse(JSON.stringify(obj));}const original = {a: 1,b: {c: 2}}const copy = deepCopy(original);original.a = 4;console.log("原始数据")console.log(original);console.log("深拷贝后的数据")console.log(copy);
</script>
这种方法的优点是简单易用,缺点是:
- 不能处理函数、日期对象、正则表达式等特殊类型的对象
- 不能处理循环引用的对象
- 会忽略
undefined
、Function
、Symbol
等类型的属性。
- 手动实现
对于更复杂的对象,可以手动实现深拷贝,这样可以处理各种类型的对象,并且可以处理循环引用的情况。
<script>function isObject(item){return (typeof item === 'object' && !Array.isArray(item) && item !== null);}function deepClone(obj,hash = new WeakMap()){if(!isObject(obj)) return obj;if(obj instanceof Date) return new Date(obj);if(obj instanceof RegExp) return new RegExp(obj); if(hash.has(obj)) return hash.get(obj);let cloneObj = new obj.constructor();hash.set(obj,cloneObj);for(let key in obj){if(obj.hasOwnProperty(key)){if(isObject(obj[key])){cloneObj[key] = deepClone(obj[key],hash)}else{cloneObj[key] = obj[key];}}}return cloneObj;}const original = {a:1,b:{c:2}};const copy = deepClone(original);original.a = 3;console.log("原数据为:")console.log(original);console.log("深拷贝的数据")console.log(copy);
</script>
在这个例子中,我们使用WeakMap
来存储已经被复制的对象及其副本,以便于处理循环引用,并且支持日期对象、正则表达式等其他特殊类型的对象,检查hasOwnProperty
以确保只复制对象自身的属性,而不是继承的属性。
🎨觉得不错的话记得点赞收藏呀!!🎨
😀别忘了给我关注~~😀