😊JS面试八股文(一)
- 1.JS由哪三部分组成?
- 2.JS有哪些内置对象?
- 3.操作数组的方法有哪些?
- 4.JS对数据类型的检测方式有哪些?
- 5.说一下闭包,闭包有什么特点?
- 6.前端的内存泄漏怎么理解?
- 7.事件委托是什么?
- 8.基本数据类型和引用数据类型的区别?
- 9.说一下原型链
- 10.new操作符具体做了什么?
😊各位小伙伴们,本专栏新文章出炉了!!!
1.JS由哪三部分组成?
核心语法(ECMAScript
):JS的核心内容,描述了语言的基础语法和主要功能,比如var
、for
,数据类型(数组、字符串等)
文档对象模型(DOM):提供了一个接口,允许JavaScript操作HTML和XML文档。它定义了访问和操作页面内容的方法和属性。通过DOM,我们可以改变页面的结构、样式或者内容,并且相应用户的操作(如点击事件)。
浏览器对象模型(BOM):定义了JavaScript与浏览器交互的方式,提供了浏览器窗口、位置、历史记录等功能,允许脚本执行一些常见的浏览任务。
2.JS有哪些内置对象?
核心语言对象:
- String:表示文本
concat()拼接字符串
、slice()字符串截取
、split()分割字符串
- Array:用于创建
- Object:所有其他对象类型的
- Boolean:表示逻辑值(布尔值)
true
或false
- Number:表示数值
- Function:用于表示函数
- Math:提供数学常量和函数
abs()求绝对值
、sqrt()求平方根
、max()求最大值
、min()求最小值
等等、、
- Date:用于处理时间和日期
- 等等…
全局对象:
- globalThis:在全局范围内可访问的对象,包含了全局属性和方法,如
console
、setTimeout
等 - window:在浏览器环境中,
globalThis
对象是window
对象,在非浏览器环境中(如Node.js
),全局对象是global。
其他内置构造函数:
- Promise:用于处理异步操作
- Map和Set:提供键值对存储和唯一元素合集。
- 等等…
DOM相关对象:
- Document:代表当前文档
- Element:代表文档中的元素
- Node:代表文档中的节点
- Event:用于事件处理
3.操作数组的方法有哪些?
添加和删除元素
push()
:向数组末尾添加一个或多个元素,并返回新数组的长度。
<script>var array = [1,2,3]array.push(4,5) // 此时数组内元素为[1,2,3,4,5]console.log(array)
</script>
pop()
:移除数组的最后一个元素,并返回该元素。
<script>var array = [1,2,3]console.log(array.pop()); //此时控制台输出为3,array元素为[1,2]console.log(array);
</script>
unshift()
:在数组的开头添加一个或多个元素,并返回新的数组长度
<script>var array = [1,2,3]array.unshift(-1,0) //此时数组内的元素为[-1,0,1,2,3]console.log(array);
</script>
shift()
:移除数组的第一个元素,并返回该元素
<script>var array = [1,2,3]console.log(array.shift()); //此时控制台输出3,数组中的元素为[1,2]console.log(array);
</script>
插入和提取片段
splice()
:可以用来删除、替换、或插入数组中的元素
- 删除:
<script>var array = [1,2,3,4,5]array.splice(1,2) //此时表示,从数组索引为1的位置开始,删除两位console.log(array); //此时数组元素还剩[1,4,5]
</script>
- 插入:
<script>var array = [1,2,3,4,5]array.splice(1,0,22,33) //此时表示在数组索引为1的位置前插入22和33两个元素//请注意,此时deleteCount 参数为0,表示不会有任何元素被删除console.log(array); //此时数组内元素应为[1,2,22,33,4,5]
</script>
替换元素:
<script>var array = [1, 2, 3, 4, 5]array.splice(2, 1, 100) //此时表示将数组索引为2的元素替换为100console.log(array); //此时数组内元素为[1,2,100,4,5]
</script>
并且splice()
:方法还可以返回一个数组,其中包含被删除的元素
<script>var array = [1, 2, 3, 4, 5]var removeElement = array.splice(2,2); //此时removeElement内的数据为[3,4]console.log("原数组的值为",array);console.log("删除的元素为",removeElement);
</script>
slice()
:截取元素,返回一个新的数组对象,请注意,使用这个方法得到的数组是原数组的浅拷贝。
<script>var array = [1, 2, 3, 4, 5]var newArray = array.slice(1,3) //此时得到的新数组元素为[2,3]// 需要注意的是,提取的元素范围是起始索引到结束索引的前一个位置,// 也就是说,上述是从索引1提取到索引3,所提取的元素只包含索引1和索引2,不包括索引为3的元素console.log(newArray);
</script>
查找和排序
indexOf()
:返回数组中第一个匹配指定值的元素的索引,如果没找到则返回-1
<script>var array = [1,2,3,4,5]var number = array.indexOf(2) //此时,如果数组中存在此元素,那么则会输出他的下标console.log("元素的下标为:",number);
</script>
lastIndexOf()
:返回数组中最后一个匹配指定值的元素的索引
<script>var array = [1,2,2,4,5,5]var number = array.lastIndexOf(5) //此时,如果数组中存在两个相同的元素// 那么,会取到这个元素最后一次的下标// 所以,元素5的最后一次下标应该为5console.log("元素的下标为:",number);
</script>
includes()
:判断某个数组是否包含某个值,根据判断结果返回true
或false
<script>var array = [1,2,3,4,5,6]console.log(array.includes(5)) //此时,如果数组中存在5这个元素,那么就会输出true
</script>
sort()
:对数组的元素进行排序。默认情况下,sort()
方法按照字符串的Unicode码顺序对数组元素进行排序,这意味着如果你对数字数组进行排序,结果可能会出错,并不会得到真正的顺序,因此,在对数字或者其他类型的数组进行排序时,通常要提供一个比较函数来正确排序。
<script>var array = [1,43,21,32,54,56,34,23]// 此时进行升序排序array.sort(function(a,b){return a-b;})// 此时数组的顺序为升序console.log(array);console.log("===================")// 此时进行降序排序array.sort(function(a,b){return b-a;})// 此时数组的顺序为降序console.log(array);</script>
哪些方法会改变原数组?
- 会改变元素组的方法:
push()
、pop()
、shift()
、unshift()
、reverse()
、sort()
、splice()
4.JS对数据类型的检测方式有哪些?
typeof
:是一种基本类型检测手段,可以用来识别基本的数据类型,但是在识别对象数据类型的时候会返回object
。
<script>console.log(typeof 123)console.log(typeof 'string')console.log(typeof true)console.log(typeof undefined)console.log(typeof Symbol())console.log(typeof BigInt)console.log(typeof function(){})//此时,如果用于检测对象类型,只会返回Objectconsole.log("以下是对象数据类型")console.log(typeof [1,2,3])
</script>
instanceof
:可以用来检测一个对象是否为某个构造函数的实例。也就是说适用于对象数据类型,并不能适用于基本数据类型。
<script>console.log([] instanceof Array);console.log({} instanceof Object);console.log(new Date() instanceof Date);// 此时,如果用来检测基本数据类型,只会返回falseconsole.log("以下是基本数据类型")console.log("123" instanceof String)
</script>
Object.prototype.toString.call()
:这个方法可以用来获取对象的内部[[Class]]
属性,他能更加准确的检测出各种类型的数据。
<script>var value = Object.prototype.toString;console.log(value.call(123));console.log(value.call('123'));console.log(value.call(true));console.log(value.call(undefined));console.log(value.call(null));console.log(value.call([]));console.log(value.call({}));console.log(value.call(function(){}));console.log(value.call(new Date()));
</script>
constructor
是一个特殊的属性,通常用于指向创建一个对象的构造函数,这个属性存在几乎所有的内置对象上,并且可以被继承到自定义的对象上。
<script>var num = new Number(10);console.log(num.constructor === Number); // 输出: truevar str = new String('hello');console.log(str.constructor === String); // 输出: truevar arr = new Array(1, 2, 3);console.log(arr.constructor === Array); // 输出: truevar date = new Date();console.log(date.constructor === Date); // 输出: truevar regExp = new RegExp('\\d+');console.log(regExp.constructor === RegExp); // 输出: true
</script>
5.说一下闭包,闭包有什么特点?
闭包(Closure)是Javascript中的一个重要的概念,他涉及到函数和语法作用域的理解。闭包是指一个函数可以访问并操作在其外部定义的变量,即使在外部函数已经执行完毕返并返回之后也是如此。
闭包产生的条件
内部函数
:存在一个内部函数(通常是定义在一个外部函数内部的函数)。访问外部变量
:内部函数可以访问其外部函数的作用域中的变量,并且这些变量在外部函数执行完成后仍然可以被访问。
闭包的原理
- 语法作用域:在
JavaScript
中,函数的作用域是在定义时确定的,而不是在调用时确定的。这意味着函数可以访问其定义时所在的作用域中的变量。 - 作用域链:每个函数都有一个作用域链,它是一个指向变量对象的链表,用于查找变量。当一个函数被定义时,它的作用域链会包含当前作用域(局部作用域)以及外部作用域(全局作用域或其他外部函数的作用域)
- 内存引用:当一个内部函数引用了外部函数的局部变量时,Javascript引擎会为这些变量保留内存空间,直到没有引用这些变量的函数存在为止。
<script>function a(value){return function b(){console.log(value)}}var test = a("123456")test() //输出123456
</script>
在这个例子中,b
函数是a
函数内部定义的一个函数,它访问了a
函数的作用域中的变量,也就是a
函数的形参value
,当a
函数返回b
函数后,即使a
函数已经执行完毕返回了,但是b
函数仍然可以访问value
,这就是闭包。
闭包的特点
- 访问外部作用域中的变量
- 闭包可以让额你不函数访问其外部作用域内的变量,即使外部函数已经执行完毕并返回,这些变量仍然可以被访问。这是因为闭包保留了对外部作用域的引用。
<script>function outer() {var x = 10;function inner() {console.log(x); // 访问外部变量 x}return inner;}var closureFunc = outer();closureFunc(); // 输出: 10
</script>
- 保持状态
- 闭包可以让内部函数保持对外部和变量的引用,从而实现状态的持久化。在实现计数器,缓存等场景中非常有用。
<script>function createCounter() {var count = 0;return function () {count += 1;console.log(count);};}var counter = createCounter();counter(); // 输出: 1counter(); // 输出: 2</script>
- 私有变量
- 在JavaScript中没有私有成员的概念,但是通过闭包可以模拟私有变量。只有闭包内部函数可以访问这些变量,外部无法直接访问。
<script>function createSecretKeeper() {var secret = "This is a secret";return {getSecret: function () {return secret;}};}var keeper = createSecretKeeper();console.log(keeper.getSecret()); // 输出: This is a secret
</script>
- 生命周期长于普通局部变量
- 由于闭包保留了对外部作用域的引用,因此这些变量的生命周期会比普通的局部变量长。如果闭包长时间不被释放,可能会导致内存泄露。
- 独立的作用域链
- 闭包函数拥有自己独立的作用域链,它可以在不干扰全局作用域的情况下访问外部变量。
使用场景
防抖,节流、保存状态,延迟执行等等。
6.前端的内存泄漏怎么理解?
前端的内存泄露是指在浏览器中执行Javascript代码时,由于某些原因导致不再使用的内存资源未能被及时释放,从而使这些资源占用的空间无法被垃圾回收机制回收。这种情况会导致浏览器使用的内存逐渐增加,形成内存资源的浪费,最终可能导致性能下降甚至崩溃。
- 内存泄漏的原因
- 闭包
- 循环引用:常见两个对象相互引用
- 事件监听器:未能及时解绑
- DOM元素引用:函数对DOM元素的引用,且未被解绑
- 定时器和回调
7.事件委托是什么?
事件委托是一种常用的事件处理机制,也就是把子元素的事件绑定到父元素身上,这种方法可以显著减少事件处理的数量,从而提高性能。其实现关键点就是在于利用事件冒泡机制,使父级元素可以捕获到子元素触发的事件。
事件委托的工作原理
- 事件冒泡:当一个事件在子元素上发生时,它会向上冒泡至祖先元素。
- 选择器匹配:通过检查事件发生的目标元素(即触发事件的实际元素),判断该元素是否符合设定的选择器条件,进而决定是否执行相应的事件处理逻辑。
事件委托的优势
- 减少事件处理器的数量
- 动态元素:对于动态生成的元素(例如通过
Ajax
加载的内容),事件委托可以确保这些元素也能自动受到事件处理逻辑的影响。 - 性能提升:减少了事件监听器的数量,提高了页面的响应速度和整体性能。
<div id="container"><button class="btn">按钮1</button><button class="btn">按钮2</button><button class="btn">按钮3</button><button class="btn">按钮4</button>
</div>
<script>document.getElementById("container").addEventListener('click',function(event){var target = event.target;if(target.classList.contains("btn")){console.log("点击了按钮",target.textContent);}});
</script>
在这个例子中,并没有为每个按钮都单独都绑定点击事件处理器,而是将事件处理器绑定到了包含所有按钮的容器div
上,当用户点击任何一个按钮时,事件都会冒泡到div
容器上,然后通过检查event.target
并使用.classList.contains('btn')
来判断是否点击的是按钮,并执行相应的逻辑。
8.基本数据类型和引用数据类型的区别?
基本数据类型
number
:数值类型,包括整数和浮点数。string
:字符串类型。boolean
:布尔类型,有两个值true
和false
null
:表示没有任何对象值undefined
:表示一个未定义的值bigint
:整数类型,可以表示任意大的整数symbol
:符号类型,用于创建唯一的键
基本数据类型的特点
- 值存储:基本数据类型在内存中直接存储其值。
- 赋值:当一个基本数据类型的值赋给另一个变量时,实际上是复制了该值。
- 不可变性:基本数据类型的值是不可变的,一旦被创建就不能被更改
引用数据类型
引用数据类型主要包括对象(Object),数组(Array)、函数(Function)等,他们都是基于对象的类型。
引用数据类型的特点
- 值存储:引用数据类型的值存储的是一个指针,指向内存中的某个位置,而不是直接存储值。
- 赋值:当一个引用类型的值赋给另一个变量时,实际上是复制了这个指针,两个变量指向同一个内存地址。
- 可变性:引用数据类型的值是可以改变的,可以通过修改其属性来改变对象的状态。
深拷贝与浅拷贝
由于引用数据类型赋值时仅仅是复制了引用,所以在某些情况下需要创建一个完全独立的副本。这个时候就需要用到深拷贝和浅拷贝。浅拷贝是仅复制对象的第一层属性。深拷贝是完整地复制对象的所有层级。
<script>let obj1 = {name:'张三',friends:['李四','王五']};let shallowCopy = Object.assign({},obj1); //浅拷贝//此时如果obj1发生改变,那么shallowCopy也会发生改变obj1.friends.push("添加的数据")console.log(shallowCopy.friends); //输出结果为["李四","王五","添加的数据"]console.log("----------------------")//此时obj1.friends中包含的数据为["李四","王五","添加的数据"]let deepCopy = JSON.parse(JSON.stringify(obj1)); //深拷贝//此时再往obj1.friends中添加数据,deepCopy中是不会受到影响的obj1.friends.push("测试数据2");console.log(deepCopy.friends) //输出结果为["李四","王五","添加的数据"]</script>
9.说一下原型链
想要了解原型链,就得先了解原型(Prototype
),它用于实现对象之间的属性继承和方法共享。原型机制是JavaScript中实现继承的基础,也是理解对象行为的关键。
在JavaScript中,每个对象都有一个特殊的内部属性prototype
,这个属性指向另一个对象,称为该对象的“原型”,原型对象本身也可以有自己的原型,就形成了一个原型链。__proto__
可以理解为一个指针,是实例对象中的属性,指向了构造函数的原型(prototype
)。
访问机制
当我们试图访问一个对象的某个属性或方法时,如果该对象自身没有这个属性或方法,JavaScript就会沿着该对象的原型链查找,直到找到该属性或方法为止。如果整个原型链中都没有此属性或方法,就会返回undefined
。
构造函数与原型
在JavaScript中,构造函数的prototype
属性是一个特殊的对象,它也是所有由该构造函数创建的实例的原型。
<script>function Person(name){this.name = name;}//给这个构造函数的原型上添加属性Person.prototype.getName = function(){console.log('这是创建实例后传入的姓名'+this.name)}//此时我们查看控制台,Person这个构造函数是没有getName这个属性的console.log(Person);//创建实例并调用getName属性,则会顺着原型链查找此属性let person = new Person("张三")person.getName();
</script>
原型链
当一个对象访问某个属性或方法时,JavaScript会沿着该对象的原型链查找。原型链由对象及其原型对象构成,依次向上查找,直到找到所需要的属性或方法为止。
<script>function Person(name,age) {this.name = name;this.age = age}//给这个构造函数的原型上添加属性Person.prototype.getName = function () {console.log('我的名字是' + this.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>
在这个例子中,Person
是一个基类构造函数,Student
是一个派生类构造函数,它继承了Person
的属性和方法。然后我们设置Student。prototype
的原型为Person.prototype
,因此可以访问Person原型上的属性和方法。
其中至关重要的一步Student。prototype,constructor=Student
,这是将Student.prototype.constructor
属性设置为Student
构造函数本身,设置这一步的目的是为了确保通过Student
构造函数创建的所有实例,在访问constructor
属性都能正确的指向Student
构造函数本身,因为在上一步使用了Object.create()
方法设置原型,原型对象默认的constructor
属性会被设置为Object.prototype.constructor
,即Function.prototype
的一个实例。
10.new操作符具体做了什么?
- 创建新对象
new
操作符首先会创建一个空的新对象。
- 设置原型
- 新创建的对象的原型会被设置为构造函数的
prototype
属性所指向的对象。这是为了让新对象能够继承构造函数原型上的方法和属性。
- 新创建的对象的原型会被设置为构造函数的
- 绑定
this
- 构造函数被调用时,其内部的
this
关键字会被绑定到新创建的对象上。构造函数内部可以使用this
关键字来设置新对象的属性。
- 构造函数被调用时,其内部的
- 执行构造函数
- 使用新创建的对象作为上下文来调用构造函数。
- 返回新对象
- 如果构造函数显式地返回一个对象,则
new
操作符返回该对象。果然构造函数没有返回任何对象(或返回undefined
),则new
操作符返回新创建的对象。
- 如果构造函数显式地返回一个对象,则
<script>function Person(name,age){this.name = name;this.age = age}Person.prototype.get = function(){console.log("我的姓名是",this.name,",我的年龄是",this.age);}let person = new Person("张三",18);person.get();
</script>
🎨觉得不错的话记得点赞收藏呀!!🎨
😀别忘了给我关注~~😀