1.JS由哪三部分组成?
JavaScript 主要由以下三部分组成:
核心(ECMAScript):
ECMAScript 是 JavaScript 语言的标准和核心。它定义了语言的语法、类型、语句、关键字、保留字、操作符、对象以及它们的行为。这部分是所有 JavaScript 实现的基础,无论在什么平台上。
文档对象模型(DOM):
文档对象模型(DOM)是一个跨平台和语言独立的接口,允许程序和脚本动态地访问和更新文档的内容、结构和样式。在 Web 浏览器中,JavaScript 通过 DOM 提供的 API 与 HTML 文档进行交互,例如创建、添加或修改 HTML 元素和属性。
浏览器对象模型(BOM):
浏览器对象模型(BOM)允许 JavaScript 与浏览器进行交互,而不是文档的内容。例如,通过 BOM,开发者可以控制浏览器窗口、读取和设置浏览器的导航历史、进行异步通信等。BOM 提供了很多对象,用于访问浏览器功能,但这些功能并没有标准的规范,因此在不同浏览器中的实现可能会有所不同。
2.JS有哪些内置对象?
JavaScript 提供了许多内置对象,这些对象提供了基本的语言功能和工具,使得开发者可以执行各种常见的编程任务。这里是一些最常用的 JavaScript 内置对象:
全局对象:
Global:这不是一个真正的对象,而是全局命名空间的一个伪对象,它的属性和方法可以在代码的任何地方访问。
基本对象:
Object:所有 JavaScript 对象的基类。
Function:所有 JavaScript 函数的基类。
Boolean:表示布尔值(true 或 false)的对象。
Symbol:表示独一无二的标识符的数据类型。
错误对象:
Error:基本错误对象。
TypeError、RangeError、ReferenceError 等:具体的错误类型对象。
数字和日期对象:
Number:表示数值的对象。
Math:提供数学运算功能和常数的对象。
Date:提供日期和时间功能的对象。
字符串处理对象:
String:表示和操作字符串的对象。
RegExp:表示正则表达式的对象。
集合类型对象:
Array:表示数组的对象。
Map:表示键值对集合的对象。
Set:表示值的集合的对象。
WeakMap 和 WeakSet:分别为 Map 和 Set 的弱引用版本。
结构化数据对象:
JSON:用于解析和字符串化 JSON 数据的对象。
控制抽象对象:
Promise:用于异步编程的对象。
Generator:使得函数可以暂停执行和恢复执行的对象。
反射和代理对象:
Reflect:提供了操作对象的底层 API。
3.操作数组的方法有哪些?
JavaScript 提供了丰富的数组操作方法,使得处理和操作数组变得非常方便和高效。下面是一些最常用的数组方法:
添加或删除元素
push(): 在数组的末尾添加一个或多个元素,并返回新的长度。
pop(): 移除数组的最后一个元素,并返回那个元素。
shift(): 移除数组的第一个元素,并返回那个元素。
unshift(): 在数组的开始添加一个或多个元素,并返回新的长度。
splice(): 通过删除现有元素和/或添加新元素来更改数组的内容。
迭代元素
forEach(): 对数组的每个元素执行一次提供的函数。
map(): 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后的返回值。
filter(): 创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
reduce(): 对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
reduceRight(): 与 reduce() 类似,但是从数组的末尾向前工作。
查找元素
indexOf(): 返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
lastIndexOf(): 返回在数组中可以找到一个给定元素的最后一个索引,如果不存在,则返回-1。
find(): 返回数组中满足提供的测试函数的第一个元素的值,否则返回 undefined。
findIndex(): 返回数组中满足提供的测试函数的第一个元素的索引,否则返回 -1。
测试元素
every(): 测试一个数组内的所有元素是否都能通过某个指定函数的测试。
some(): 测试数组中的某些元素是否通过由提供的函数实现的测试。
排序和反转
sort(): 对数组的元素进行排序,并返回数组。
reverse(): 颠倒数组中元素的顺序。
其他实用方法
concat(): 用于合并两个或多个数组。
join(): 将数组中的所有元素连接成一个字符串并返回这个字符串。
slice(): 返回一个新的数组对象,这一对象是一个由开始到结束(不包括结束)选择的数组的一部分浅拷贝。
flat(): 用于将嵌套的数组“拉平”,变成一维的数组。
flatMap(): 先使用映射函数映射每个元素,然后将结果压缩成一个新数组。
这些方法可以单独使用或者结合使用,以实现复杂的数组操作和数据处理。
4.JS对数据类的检测方式有哪些?
在 JavaScript 中,检测数据类型主要涉及到各种数据,如基本数据类型(如字符串、数字等)和对象类型(如数组、函数等)。下面是一些常用的数据类型检测方法:
typeof
操作符
typeof
是一个一元操作符,用来返回一个表示操作数数据类型的字符串。它适用于基本类型的检测,但对于对象类型,特别是区分数组和对象,typeof
可能就不太适用了。
console.log(typeof "hello"); // "string"
console.log(typeof 42); // "number"
console.log(typeof true); // "boolean"
console.log(typeof undefined);// "undefined"
console.log(typeof Symbol()); // "symbol"
console.log(typeof function() {}); // "function"
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof null); // "object"
instanceof
操作符
instanceof
检测构造函数的 prototype
属性是否出现在对象的原型链中的任何位置。这对于自定义类型或内置类型对象非常有用,但不适用于基本数据类型。
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function() {} instanceof Function); // true
Object.prototype.toString.call()
Object.prototype.toString.call()
是一种更可靠的类型检测方法,能够区分不同的对象类型,如数组、日期等。
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
Array.isArray()
特别为数组设计的检测方法,用来确定传递的值是否是一个数组。这是检测数组的最可靠方式。
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
构造函数名称 constructor
通过检查对象的 constructor
属性来确定对象的类型。但这种方法的可靠性取决于 constructor
属性没有被改变。
console.log(([]).constructor === Array); // true
console.log(({}).constructor === Object); // true
console.log((function() {}).constructor === Function); // true
5.说一下闭包,闭包有什么特点?
在JavaScript中,闭包是一个函数和其被创建时的词法环境的组合。这个特性使得一个函数即使在其外部作用域被执行时,仍然能访问到它定义时的作用域中的变量。闭包是JavaScript中非常强大的一个特性,可以用于多种用途,如数据隐藏和封装、创建模块或函数库等。
示例:使用闭包来封装数据
下面是一个利用闭包来封装数据,使其不能直接从外部被访问或修改的例子:
function createBankAccount(initialBalance) {
let balance = initialBalance; // balance是一个通过闭包保护的私有变量
return {
deposit: function(amount) {
balance += amount; // 内部函数可以访问外部函数的balance变量
return balance;
},
withdraw: function(amount) {
if (amount <= balance) {
balance -= amount;
return balance;
} else {
return 'Insufficient funds';
}
},
getBalance: function() {
return balance; // 提供了一个公共的方法来查看余额
}
};
}
const account = createBankAccount(100);
console.log(account.getBalance()); // 输出: 100
account.deposit(50);
console.log(account.getBalance()); // 输出: 150
console.log(account.withdraw(20)); // 输出: 130
在这个例子中,函数createBankAccount
创建了一个具有存款、取款和查询余额功能的对象。这个对象中的方法都可以访问定义它们的函数作用域中的balance
变量。这个变量对于外部代码是不可见的,只能通过这三个方法进行操作,有效地隐藏了实现细节并防止了外部直接修改余额。
闭包的典型用途
- 数据封装:正如上面的银行账户示例所示,闭包可以用于封装私有数据,提供操作这些数据的公共方法。
- 模块化代码:闭包可以用来创建模块,封装一组具有共同功能的方法,只暴露必要的API给外部使用,其余细节保持私有。
- 函数工厂:闭包可以用来创建设置特定参数的特定功能的函数。
- 记忆功能:闭包可以用来缓存数据,例如在递归计算中缓存中间结果,提高效率。
6.前端的内存泄漏怎么理解?
前端的内存泄漏指的是在Web应用程序中,由于不正确的内存管理导致内存占用持续增加,直到达到浏览器或设备内存的极限,从而影响应用程序的性能甚至导致崩溃。内存泄漏通常是由于未正确释放不再需要的内存空间而导致的。
在前端开发中,常见的导致内存泄漏的情况包括:
-
未释放事件监听器:如果在DOM元素上绑定了事件监听器,但是在元素被移除或销毁之前没有及时移除这些监听器,就会导致内存泄漏。因为事件监听器仍然保留了对元素的引用,即使元素被移除,它也不会被垃圾回收。
-
循环引用:当对象之间存在相互引用,而且这些引用形成了一个循环,即使这些对象已经不再被应用程序所需要,它们也不会被垃圾回收。这种情况下,需要手动断开循环引用,或者使用弱引用(WeakMap、WeakSet)来避免内存泄漏。
-
定时器和异步操作:未清除的定时器或异步操作(如Promise、事件监听器等)会阻止相关的作用域被垃圾回收,导致内存泄漏。特别是在单页应用程序(SPA)中,定时器和异步操作的数量可能会快速增加,因此需要谨慎管理它们的生命周期。
-
闭包:当闭包中引用了外部作用域的变量,并且这些闭包长期存在,而且外部作用域中的变量无法被释放,就会导致内存泄漏。
为了避免内存泄漏,开发人员应该注意以下几点:
- 及时释放不再需要的引用,包括事件监听器、定时器、异步操作等。
- 避免创建不必要的全局变量,使用局部变量和合适的作用域来限制变量的生命周期。
- 使用工具和性能分析器来检测和解决内存泄漏问题,如Chrome开发者工具的Memory面板。
- 注意循环引用和闭包,避免它们导致的内存泄漏。
7.事件委托是什么?
事件委托是一种常见的前端开发技术,也称为事件代理。它利用事件冒泡的机制,将事件处理程序绑定到父元素上,而不是直接绑定到子元素。当子元素触发了特定类型的事件时,事件会冒泡到父元素,父元素上绑定的事件处理程序会被调用。
事件委托的基本原理是利用了DOM事件的冒泡机制。当子元素触发一个事件时,这个事件会一级一级地向上传播,直到传播到根元素(通常是<html>
或<body>
),期间每个祖先元素都有机会捕获并处理这个事件。
通过在父元素上绑定事件处理程序,可以利用这个冒泡机制来实现事件委托。父元素可以根据触发事件的子元素来决定如何处理事件,从而减少了事件处理程序的数量,提高了性能和代码的简洁性。
事件委托的优点包括:
-
减少事件处理程序的数量:只需要在父元素上绑定一个事件处理程序,而不是在每个子元素上都绑定一个,可以大大减少代码量。
-
动态添加或移除子元素时不需要重新绑定事件:由于事件是绑定在父元素上的,所以如果动态添加或移除了子元素,无需重新绑定事件,事件处理程序仍然会生效。
-
节省内存和提高性能:减少了事件处理程序的数量,可以降低内存占用,并提高页面的响应速度。
下面是一个简单的示例,演示了如何使用事件委托来处理子元素的点击事件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Delegation Example</title>
<style>
ul {
list-style-type: none;
}
li {
padding: 10px;
border: 1px solid #ccc;
margin-bottom: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<ul id="parent-list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<script>
document.getElementById('parent-list').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
console.log('You clicked on ' + event.target.textContent);
}
});
</script>
</body>
</html>
在这个示例中,点击父元素<ul>
中的任何一个子元素<li>
,事件都会被委托到父元素上处理。通过判断event.target
的标签名,可以确定点击的是哪个子元素,并作出相应的处理。
8.基本数据类型和引用数据类型的区别?
基本数据类型(Primitive Data Types)和引用数据类型(Reference Data Types)是编程语言中常见的两种数据类型,它们之间有一些重要的区别。
基本数据类型(Primitive Data Types):
-
存储方式:基本数据类型的值直接存储在变量所分配的内存空间中,每个变量都有一个固定大小的内存区域来存储其值。
-
拷贝方式:基本数据类型的赋值是值传递的,即将原始值直接复制到新的变量中。修改一个变量的值不会影响到另一个变量。
-
不可变性:基本数据类型的值是不可变的,一旦赋值后,其值无法被改变。
-
原始数据类型:JavaScript中的基本数据类型包括:
number
、string
、boolean
、null
、undefined
和symbol
(ES6新增)。
引用数据类型(Reference Data Types):
-
存储方式:引用数据类型的值存储在内存中的堆内存中,变量存储的是一个引用(内存地址),指向实际的数据。
-
拷贝方式:引用数据类型的赋值是引用传递的,即将原始值的引用复制到新的变量中。因此,两个变量实际上引用了同一个对象,修改一个变量的值会影响到另一个变量。
-
可变性:引用数据类型的值是可变的,因为变量存储的是对象的引用,可以修改对象的属性或内容。
-
引用数据类型:JavaScript中的引用数据类型包括:
object
、array
、function
等,以及ES6中的Map
、Set
等。
总结区别:
- 基本数据类型的值是存储在栈内存中的,而引用数据类型的值是存储在堆内存中的。
- 基本数据类型的赋值是复制值,而引用数据类型的赋值是复制引用。
- 基本数据类型的值是不可变的,而引用数据类型的值是可变的。
9.说一下原型链
原型链(Prototype Chain)是JavaScript中一个重要的概念,它描述了对象之间的继承关系和原型对象的链式结构。每个对象都有一个原型(prototype),并且可以通过原型链来访问其原型对象的属性和方法。
原型链的基本原理:
-
每个对象都有一个原型(prototype):除了基本数据类型的对象外,JavaScript中的每个对象(包括函数)都有一个原型对象。可以通过
__proto__
属性来访问对象的原型。 -
对象之间通过原型链连接:当访问对象的属性或方法时,如果当前对象没有找到对应的属性或方法,它会沿着原型链向上查找,直到找到或者到达原型链的顶端(
Object.prototype
)。 -
原型链的顶端是Object.prototype:所有对象的原型链的顶端都指向
Object.prototype
。Object.prototype
是JavaScript中的基础对象,包含了一些通用的方法,如toString()
、valueOf()
等。 -
继承和属性访问:当访问对象的属性或方法时,如果对象本身没有该属性或方法,则会沿着原型链向上查找,直到找到或者到达原型链的顶端。如果在原型链上的某个原型对象中找到了对应的属性或方法,那么就会返回该属性或方法,否则返回
undefined
。
代码示例:
// 创建一个对象 obj,它的原型是 Object.prototype
const obj = {};
// obj 继承了 Object.prototype 中的属性和方法
obj.toString(); // 调用 Object.prototype 上的 toString 方法
// 创建一个函数 func,它的原型是 Function.prototype
function func() {}
// func 继承了 Function.prototype 中的属性和方法
func.call(); // 调用 Function.prototype 上的 call 方法
// 所有对象的原型链的顶端都指向 Object.prototype
console.log(obj.__proto__ === Object.prototype); // 输出: true
console.log(func.__proto__ === Function.prototype); // 输出: true
// Object.prototype 的原型是 null,表示原型链的顶端
console.log(Object.prototype.__proto__ === null); // 输出: true
在这个示例中,obj
对象和func
函数都继承了它们各自原型对象中的属性和方法,如果在对象或函数自身找不到对应的属性或方法,就会沿着原型链向上查找,直到找到或者到达原型链的顶端。
10.new操作符具体做了什么?
new
操作符用于创建一个新的对象实例,它执行了以下几个步骤:
-
创建一个新的空对象:
new
操作符首先创建一个新的空对象,这个对象将成为新创建的实例。 -
将新对象的原型(
__proto__
)链接到构造函数的原型对象:new
操作符将新创建的对象的原型属性(__proto__
)链接到构造函数的原型对象上。这样,新创建的对象就可以访问构造函数原型对象上的属性和方法。 -
将构造函数的作用域赋给新对象(绑定
this
):通过apply
、call
方法或者bind
方法,将构造函数的作用域绑定到新创建的对象上,使得构造函数中的this
关键字指向新创建的对象。 -
执行构造函数的代码块:此时,新对象已经创建并且
this
指向了这个新对象,然后执行构造函数中的代码块。构造函数中的属性和方法可以被添加到新对象上。 -
返回新对象:如果构造函数没有显式地返回一个对象,则
new
操作符会隐式地返回新创建的对象实例。如果构造函数中显式地返回了一个对象,则new
操作符返回该对象。
代码示例:
// 定义一个构造函数 Person
function Person(name, age) {
this.name = name;
this.age = age;
}
// 使用 new 操作符创建一个新的对象实例
const person1 = new Person('Alice', 30);
console.log(person1.name); // 输出: Alice
console.log(person1.age); // 输出: 30
在这个示例中,new Person('Alice', 30)
创建了一个新的对象实例 person1
。new
操作符创建了一个新的空对象,并将构造函数 Person
的作用域绑定到这个新对象上,然后执行构造函数中的代码块,将属性 name
和 age
添加到新对象上,最后返回这个新对象。