前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
JavaScript基础知识
JavaScript有哪些数据类型,它们的区别?
Number
(数字): 用于表示数值,可以是整数或浮点数。例如:42、3.14。String
(字符串): 用于表示文本数据,可以用单引号、双引号或反引号括起来。例如:"Hello"、'World'、"JavaScript"
。Boolean
(布尔): 用于表示逻辑值,只有两个可能的值:true和false。Undefined
(未定义): 表示变量声明了但没有赋值,或者访问不存在的属性时返回的值。Null
(空值): 表示一个空值或者不存在的对象。Symbol
(符号): 是ES6引入的一种数据类型,用于创建唯一的、不可变的值,通常用于对象属性的唯一标识符。BigInt
(大整数): 也是ES11引入的一种数据类型,用于表示任意精度的整数,可以处理超出Number类型表示范围的整数。- 除了基本数据类型,JavaScript还有一种复杂数据类型:
Object
(对象): 用于表示复杂数据结构,可以包含多个键值对,每个键值对都是属性名和对应的值组成。
这些数据类型之间的区别在于它们的值特点、存储方式和操作方式
。例如,Number和String类型可以进行数学运算和字符串操作,Boolean类型用于逻辑判断,Object类型用于组织和存储数据等。理解这些数据类型的特点,有助于编写更清晰、可维护的JavaScript代码。
数据类型检测的方式有哪些?
typeof
操作符:例如typeof 42; // "number"
instanceof
操作符:例如let arr = []; arr instanceof Array; // true
Object.prototype.toString.call()
方法:例如Object.prototype.toString.call("Hello"); // "[object String]"
constructor
方法:例如console.log(('str').constructor === String); // true
Array.isArray()
方法:该方法用于判断是否为数组Array.isArray([]); // true
null和undefined区别是什么?
undefined
:
undefined 表示一个声明了但没有被赋值的变量,或者访问一个不存在的属性或变量时返回的值。
当声明一个变量但未初始化时,它的值默认为 undefined。
函数没有返回值时,默认返回 undefined。
在代码中,不要主动将变量赋值为 undefined,而应该让它自然成为未赋值状态。
null
:
null 表示一个明确的、空的值,表示此处应该有一个值,但是当前还没有。它是一个赋值操作,表示将变量设置为空值。
null 通常用来显示地表示变量被清空了,或者某个属性被清空了。
在代码中,可以使用 null 来显式地指示变量或属性为空。
数据类型之间转换规则是什么?
其他值转字符串
- Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
- Boolean 类型,true 转换为 "true",false 转换为 "false"。
- Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
- Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
- 对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()
(Object.prototype.toString())
来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
其他值转数字
- Undefined 类型的值转换为 NaN。
- Null 类型的值转换为 0。
- Boolean 类型的值,true 转换为 1,false 转换为 0。
- String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
- Symbol 类型的值不能转换为数字,会报错。
- 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
其他值转布尔
以下这些是假值: • undefined • null • false • +0、-0 和 NaN • ""
显式、隐式类型转换区别是什么?
JavaScript 中的类型转换分为显式类型转换(Explicit Type Conversion,也称为类型转换或类型强制转换)和隐式类型转换(Implicit Type Conversion,也称为类型转换或类型强制转换)。
显式类型转换:
显式类型转换是通过代码明确地使用一些函数或操作符来将一个数据类型转换为另一个数据类型。这种转换是由开发人员手动进行的,以满足特定的需求。例如:
let num = 42; let str = String(num); // 显式地将数字转换为字符串 let bool = Boolean(str); // 显式地将字符串转换为布尔值
隐式类型转换:
隐式类型转换是在操作过程中自动发生的,JavaScript 在需要不同类型的值时,会根据规则自动执行类型转换。这种转换不需要显式的开发人员干预,JavaScript 引擎会自动处理。例如:
let num = 42; let str = num + ""; // 隐式地将数字转换为字符串 let bool = !num; // 隐式地将数字转换为布尔值
区别:
- 操作方式:
显式类型转换是由开发人员明确地编写代码来执行的,使用特定的函数或操作符。
隐式类型转换是在表达式求值过程中自动执行的,由 JavaScript 引擎根据表达式的操作和数据类型进行推断。
- 可控性:
显式类型转换可以让开发人员在需要的时候有更精确的控制,确保类型转换的结果符合预期。
隐式类型转换是自动发生的,有时可能导致意外的结果,需要开发人员注意避免潜在的问题。
- 代码可读性:
显式类型转换通常会使代码更加清晰,因为它明确地表达了开发人员的意图。
隐式类型转换可能会让代码更加简洁,但有时可能降低代码的可读性,需要注意平衡。
什么是JavaScript的包装类型?
JavaScript 的包装类型是指基本数据类型(例如 number、string、boolean)在一些特定场景下会被自动转换为对应的包装对象,从而可以使用对象的方法和属性。这种自动转换是临时性的,仅在需要调用对象方法或属性时发生,操作完成后又会自动转换回基本数据类型。这种特性可以让基本数据类型在某些情况下表现得像对象。
JavaScript 中的包装类型包括:
- Number 对应于基本数据类型 number
- String 对应于基本数据类型 string
- Boolean 对应于基本数据类型 boolean
举例说明:
let num = 42; let str = "Hello"; let bool = true; console.log(typeof num); // "number" console.log(typeof str); // "string" console.log(typeof bool); // "boolean" // 使用包装类型的方法和属性 let numObj = new Number(num); console.log(numObj.toFixed(2)); // 调用 Number 对象的 toFixed() 方法 let strObj = new String(str); console.log(strObj.length); // 调用 String 对象的 length 属性 let boolObj = new Boolean(bool); console.log(boolObj.valueOf()); // 调用 Boolean 对象的 valueOf() 方法
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
JavaScript有哪些内置对象?
JavaScript 提供了许多内置对象,这些对象在语言中默认可用,无需额外导入或加载。以下是一些常见的 JavaScript 内置对象:
- Object:通用对象,所有其他对象都从它派生。
- Array:用于创建数组,存储一组有序的值。
- String:用于处理字符串,提供字符串操作的方法。
- Number:用于处理数字,提供数学操作的方法。
- Boolean:表示布尔值 true 或 false。
- Function:用于定义函数,函数是 JavaScript 的基本构建块之一。
- Date:用于处理日期和时间。
- RegExp:用于进行正则表达式匹配。
- Math:提供数学相关的常量和函数,例如三角函数、对数函数等。
- JSON:用于解析和序列化 JSON 数据。
- Map:表示键值对的集合,支持任意数据类型作为键。
- Set:表示一组唯一的值,不允许重复。
- Promise:用于处理异步操作,支持处理成功和失败状态。
- Symbol:表示唯一标识符,通常用于对象属性的键。
- ArrayBuffer:表示二进制数据的缓冲区,用于处理底层二进制数据。
- TypedArray:基于 ArrayBuffer 的一组特定数据类型的数组,例如 Int8Array、Float64Array 等。
- WeakMap:类似于 Map,但对于键的引用不会阻止垃圾回收。
- Set:表示一组唯一值的集合。
- WeakSet:类似于 Set,但对于值的引用不会阻止垃圾回收。
这只是内置对象的一部分。每个内置对象都有自己的方法和属性,用于特定的操作和功能。了解这些内置对象和它们的用法,对于编写 JavaScript 代码非常有帮助。
new操作符实际做了哪些事情?
new 操作符用于创建一个实例对象,它执行了以下步骤:
- 创建一个新的空对象:new 操作符首先会创建一个新的空对象。
- 设置对象的原型链接:新创建的对象的 [[Prototype]](隐式原型)被设置为构造函数的 prototype 属性。
- 将构造函数的上下文设置为新对象:在调用构造函数时,将 this 关键字绑定到新创建的对象,使构造函数内部的代码可以操作新对象。
- 执行构造函数的代码:构造函数内部的代码被执行,可能会在新对象上设置属性和方法。
- 如果构造函数没有返回对象,则返回新对象:如果构造函数没有显式返回一个对象,则 new 操作符会自动返回新创建的对象;如果构造函数返回一个非对象的值,则不会影响返回结果。
以下是一个使用 new 操作符创建对象的示例:
// 构造函数 function Person(name, age) { this.name = name; this.age = age; } // 使用 new 操作符创建实例对象 let person1 = new Person("Alice", 30); let person2 = new Person("Bob", 25); console.log(person1.name); // Alice console.log(person2.age); // 25
在这个示例中,new 操作符创建了两个 Person 的实例对象,分别是 person1 和 person2。在构造函数中,this 指向了这两个实例对象,使得构造函数可以在实例对象上设置属性。prototype 属性用于设置实例对象的原型链接,从而可以让实例对象通过原型链访问构造函数的原型中的属性和方法。
Map,Object,weakMap有什么区别?
Map、Object 和 WeakMap 都是 JavaScript 中用于存储键值对的数据结构,但它们之间有一些区别:
Object:
- Object 是 JavaScript 中最基本的数据结构之一,用于存储键值对,其中键必须是字符串或符号。
- Object 对象的属性没有特定的顺序,属性的访问速度较快。
- Object 可以用于创建自定义的数据结构和对象,是最常用的数据类型之一。
Map:
- Map 是一种新的数据结构,引入于 ECMAScript 6,用于存储任意类型的键值对。
- Map 中的键可以是任意数据类型,包括基本数据类型和对象引用。
- Map 中的键值对是有序的,插入顺序决定了键值对的遍历顺序。
- Map 支持迭代器,可以使用 for...of 循环遍历键值对。
- Map 通常在需要更灵活的键类型、有序键值对以及更好的迭代支持时使用。
WeakMap:
- WeakMap 也是引入于 ECMAScript 6,类似于 Map,但有一些不同之处。
- WeakMap 的键必须是对象,值可以是任意数据类型。
- WeakMap 中的键是弱引用,不会阻止对象被垃圾回收,从而避免了内存泄漏的风险。
- WeakMap 不支持迭代器,无法直接遍历键值对。
- WeakMap 通常用于需要将数据与某些对象关联起来,但又不希望阻止对象被回收的情况。
总结区别:
- Object 是基本的键值对存储结构,键是字符串或符号。
- Map 是通用的键值对存储结构,键可以是任意数据类型,支持有序遍历。
- WeakMap 是一种键值对存储结构,键是对象,具有弱引用特性,不阻止对象被回收。
选择使用哪种数据结构取决于你的需求,例如键的类型、键值对的顺序以及内存管理等。
应用场景:
Object 的应用场景:
- 创建自定义对象:Object 可以用于创建自定义的对象,用于表示特定类型的数据。
- 作为简单的映射:在一些简单的情况下,Object 可以用作简单的键值映射。
// 创建自定义对象 const person = { name: "Alice", age: 30, } // 简单的映射 const colorMap = { red: "#FF0000", green: "#00FF00", blue: "#0000FF", }
Map 的应用场景:
- 存储复杂键值对:Map 可以用于存储任意类型的键值对,例如存储函数、对象等。
- 维护插入顺序:Map 中的键值对是有序的,可以用于实现记录操作顺序的日志。
- 避免命名冲突:Map 中的键不受命名空间的限制,可以用于避免不同模块之间的命名冲突。
// 存储用户数据 const userMap = new Map(); const user1 = { name: "Alice", age: 30 }; const user2 = { name: "Bob", age: 25 }; userMap.set(user1, "User data for Alice"); userMap.set(user2, "User data for Bob"); // 记录操作顺序 const logMap = new Map(); logMap.set("1", "First action"); logMap.set("2", "Second action"); logMap.set("3", "Third action"); // 避免命名冲突 const moduleAMap = new Map(); const moduleBMap = new Map(); moduleAMap.set("data", "Module A data"); moduleBMap.set("data", "Module B data");
WeakMap 的应用场景:
- 私有数据存储:WeakMap 可以用于存储对象的私有数据,因为键是弱引用,不会阻止对象被垃圾回收。
- 元数据存储:WeakMap 可以用于存储对象的元数据,这些元数据不会影响对象的生命周期。
// 私有数据存储 const privateDataMap = new WeakMap(); class MyClass { constructor() { privateDataMap.set(this, { privateValue: 42 }); } getPrivateValue() { return privateDataMap.get(this).privateValue; } } // 元数据存储 const metadataMap = new WeakMap(); function trackMetadata(obj, metadata) { metadataMap.set(obj, metadata); }
什么是类数组,如何转换成真正的数组?
类数组(Array-like)是一种类似于数组的对象,它具有一些数组的特性,例如有一系列的数字索引和 length 属性,但它并不是真正的数组,缺少数组的方法和属性。
常见的类数组包括函数的 arguments 对象、DOM 元素列表(如 NodeList)、字符串等。虽然类数组在某些情况下可以像数组一样被访问和操作,但由于缺少数组的方法,有时可能需要将其转换为真正的数组。
以下是如何将类数组转换为真正的数组的几种方法:
使用 Array.from():
Array.from() 方法可以从类数组对象或可迭代对象创建一个新的数组。它将类数组的值复制到一个新的数组中,并返回这个新数组。
// 类数组对象 function convertToArray() { const argsArray = Array.from(arguments); console.log(Array.isArray(argsArray)); // true return argsArray; } convertToArray(1, 2, 3); // [1, 2, 3]
使用 Array.prototype.slice.call():
slice() 方法可以用于截取数组的一部分,当将 slice() 应用于类数组对象时,可以将其转换为真正的数组。
// 类数组对象 const nodeList = document.querySelectorAll("p"); const nodeListArray = Array.prototype.slice.call(nodeList); console.log(Array.isArray(nodeListArray)); // true
使用扩展运算符:
扩展运算符可以将一个可迭代对象转换为多个参数,然后通过 Array.from() 或直接使用 [] 创建新数组。
// 类数组对象 const nodeList = document.querySelectorAll("p"); const nodeListArray = [...nodeList]; console.log(Array.isArray(nodeListArray)); // true
使用 concat() 方法:
通过将类数组对象与一个空数组(或其他数组)连接,可以创建一个新数组,实现类数组转换。
// 类数组对象 const nodeList = document.querySelectorAll("p"); const nodeListArray = [].concat(nodeList); console.log(Array.isArray(nodeListArray)); // true
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
操作数组有哪些方法?
javaScript 数组有许多原生的方法,用于对数组进行操作、遍历、修改等。以下是一些常见的数组原生方法:
- push(item1, item2, ...):向数组末尾添加一个或多个元素,并返回新数组的长度。
- pop():移除并返回数组的最后一个元素。
- unshift(item1, item2, ...):向数组开头添加一个或多个元素,并返回新数组的长度。
- shift():移除并返回数组的第一个元素。
- concat(array1, array2, ...):合并多个数组或值,返回一个新的合并后的数组。
- slice(start, end):截取数组的一部分,返回一个新数组,不修改原数组。
- splice(start, deleteCount, item1, item2, ...):从数组中删除、插入或替换元素,修改原数组,并返回被删除的元素组成的数组。
- join(separator):将数组的所有元素转换为字符串,使用指定的分隔符连接。
- indexOf(item, fromIndex):查找指定元素在数组中首次出现的位置,返回索引值,如果不存在则返回 -1。
- lastIndexOf(item, fromIndex):查找指定元素在数组中最后一次出现的位置,返回索引值,如果不存在则返回 -1。
- forEach(callback(item, index, array)):遍历数组的每个元素,执行回调函数。
- map(callback(item, index, array)):创建一个新数组,其中的元素是对原数组每个元素执行回调函数的结果。
- filter(callback(item, index, array)):创建一个新数组,包含满足回调函数条件的元素。
- reduce(callback(accumulator, item, index, array), initialValue):从左到右依次处理数组的元素,将结果累积为单个值。
- reduceRight(callback(accumulator, item, index, array), initialValue):从右到左依次处理数组的元素,将结果累积为单个值。
- some(callback(item, index, array)):检测数组中是否有至少一个元素满足回调函数条件,返回布尔值。
- every(callback(item, index, array)):检测数组中的所有元素是否都满足回调函数条件,返回布尔值。
- find(callback(item, index, array)):返回数组中第一个满足回调函数条件的元素。
- findIndex(callback(item, index, array)):返回数组中第一个满足回调函数条件的元素的索引,如果不存在则返回 -1。
- sort(compareFunction(a, b)):对数组元素进行排序,可传入比较函数用于指定排序规则。
- reverse():反转数组的元素顺序,修改原数组。
- fill(value, start, end):将数组的一部分元素替换为指定的值,修改原数组。
- includes(item, fromIndex):检测数组是否包含指定元素,返回布尔值。
- isArray(obj):检测一个对象是否为数组,返回布尔值。
常见的DOM操作有哪些?
DOM(Document Object Model)操作是指通过 JavaScript 对网页中的 HTML 元素和内容进行增删改查的操作。以下是一些常见的 DOM 操作以及相应的示例:
获取元素:
- getElementById(id):根据元素的 id 获取元素。
- getElementsByClassName(className):根据类名获取一组元素。
- getElementsByTagName(tagName):根据标签名获取一组元素。
- querySelector(selector):根据选择器获取第一个匹配的元素。
- querySelectorAll(selector):根据选择器获取所有匹配的元素。
// 获取元素并修改其内容 let heading = document.getElementById("myHeading"); heading.textContent = "Hello, DOM!";
创建和插入元素:
- createElement(tagName):创建一个新的元素节点。
- appendChild(node):将一个节点添加为另一个节点的子节点。
- insertBefore(newNode, referenceNode):在指定节点之前插入一个新节点。
// 创建并插入新元素 let newParagraph = document.createElement("p"); newParagraph.textContent = "This is a new paragraph."; document.body.appendChild(newParagraph);
修改元素属性和样式:
- setAttribute(name, value):设置元素的属性值。
- getAttribute(name):获取元素的属性值。
- style.property = value:设置元素的样式属性。
// 修改元素的属性和样式 let image = document.getElementById("myImage"); image.setAttribute("src", "new-image.jpg"); image.style.width = "200px";
移除元素:
removeChild(node):从父节点中移除指定的子节点。
// 移除元素 let paragraphToRemove = document.getElementById("paragraphToRemove"); paragraphToRemove.parentNode.removeChild(paragraphToRemove);
事件处理:
- addEventListener(eventType, callback):为元素添加事件监听器。
- removeEventListener(eventType, callback):移除元素的事件监听器。
// 添加事件监听器 let button = document.getElementById("myButton"); button.addEventListener("click", function() { alert("Button clicked!"); });
for...in和for...of有什么区别?
for...in 和 for...of 都是用于迭代(遍历)数据结构中的元素,但它们在使用方式和应用场景上有一些区别。
for...in 循环:
- 用于遍历对象的可枚举属性。
- 返回的是属性名(键名)。
- 可能会遍历到对象原型链上的属性。
- 不适合遍历数组或类数组对象,因为会遍历到额外的非数字索引属性和原型链上的属性。
const obj = { a: 1, b: 2, c: 3 }; for (const key in obj) { console.log(key); // 输出:a, b, c }
for...of 循环:
- 用于遍历可迭代对象(如数组、字符串、Set、Map 等)的值。
- 返回的是集合中的值,而不是属性名。
- 不会遍历对象的属性,只能遍历实际值。
- 可以直接遍历数组等数据结构,适合循环迭代。
const arr = [1, 2, 3]; for (const value of arr) { console.log(value); // 输出:1, 2, 3 }
区别总结:
- for...in 遍历对象属性,返回属性名。
- for...of 遍历可迭代对象的值,返回实际值。
- for...in 可能会遍历到原型链上的属性,适合遍历对象属性。
- for...of 不会遍历原型链上的属性,适合遍历实际值。
常见的位运算符有哪些?其计算规则是什么?
按位与(&):
计算规则:对两个操作数的每个对应位执行逻辑 AND 操作,结果中的每个位都取决于两个操作数的对应位是否都为 1。
示例:a & b 返回一个值,其中每个位都是 a 和 b 对应位的 AND 结果。
按位或(|):
计算规则:对两个操作数的每个对应位执行逻辑 OR 操作,结果中的每个位都取决于两个操作数的对应位是否有至少一个为 1。
示例:a | b 返回一个值,其中每个位都是 a 和 b 对应位的 OR 结果。
按位异或(^):
计算规则:对两个操作数的每个对应位执行逻辑 XOR 操作,结果中的每个位都取决于两个操作数的对应位是否不同。
示例:a ^ b 返回一个值,其中每个位都是 a 和 b 对应位的 XOR 结果。
按位非(~):
计算规则:对操作数的每个位执行逻辑 NOT 操作,即对每个位取反,1 变为 0,0 变为 1。
示例:~a 返回一个值,其中每个位都是 a 每个位取反的结果。
左移(<<):
计算规则:将操作数的二进制位向左移动指定的位数,右侧用 0 填充。
示例:a << b 返回一个值,其中 a 的二进制位向左移动 b 位。
右移(>>):
计算规则:将操作数的二进制位向右移动指定的位数,左侧用符号位的值填充。
示例:a >> b 返回一个值,其中 a 的二进制位向右移动 b 位。
无符号右移(>>>):
计算规则:将操作数的二进制位向右移动指定的位数,左侧用 0 填充。
示例:a >>> b 返回一个值,其中 a 的二进制位向右移动 b 位,左侧用 0 填充。
解释性语言和编译型语言的区别是什么?
解释性语言和编译型语言是两种不同的编程语言类型,它们的主要区别在于代码的执行方式和编译过程:
解释性语言:
- 执行方式:解释性语言的代码在运行时逐行被解释器翻译成机器代码,然后立即执行。代码是一边翻译一边执行,不需要先进行显式的编译过程。
- 执行效率:由于每次运行都需要进行解释,解释性语言通常比较慢。解释性语言更注重开发速度和动态性,适用于一些快速原型开发和脚本编写场景。
- 跨平台性:解释性语言的代码通常可以在不同平台上直接运行,不需要重新编译。
- 例子:
Python、JavaScript、Ruby、PHP
是一些常见的解释性语言。
编译型语言:
- 编译过程:编译型语言的代码需要在运行之前先通过编译器进行一次完整的编译,将源代码翻译成目标平台的机器代码。这个编译过程产生一个可执行文件,运行时不需要再次编译。
- 执行效率:由于代码已经编译成机器代码,编译型语言的执行速度通常较快。适合开发需要高性能的应用程序。
- 跨平台性:编译型语言的代码需要为每个不同的目标平台进行编译,因此需要重新编译适应不同的操作系统和硬件。
- 例子:
C、C++、Rust、Go
是一些常见的编译型语言。
总结区别:
- 解释性语言在运行时逐行解释并执行代码,执行速度较慢,适合开发快速原型和脚本。
- 编译型语言在运行之前先编译成机器代码,执行速度较快,适合开发需要高性能的应用。
- 解释性语言跨平台性较好,无需重新编译。
- 编译型语言需要为每个平台重新编译,较难实现跨平台。
JavaScript高级知识
什么是原型,原型链?
JavaScript 中的原型(prototype)和原型链(prototype chain)是理解对象和继承机制的关键概念。
原型(prototype):
每个 JavaScript 对象都有一个关联的原型对象,它是一个普通对象,包含一些共享的属性和方法。当你访问对象的属性或方法时,如果对象本身没有这个属性或方法,JavaScript 引擎会沿着对象的原型链去查找。对象可以通过 prototype 属性来访问它的原型对象。
原型链(prototype chain):
原型链是由一系列连接的原型对象组成的链。当访问对象的属性或方法时,如果对象本身没有找到,引擎会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端(Object.prototype)。这样的搜索路径就构成了原型链。
例如:
class Person { constructor(name, age) { this.name = name this.age = age } sayHello() { console.log('hello') } } const person = new Person('John', 20) console.log(Person.prototype) console.log(person.__proto__) const person2 = Object.create(person) console.log(person2.__proto__.__proto__) person2.sayHello()
执行结果如下
这里的Person
类就存在自己的原型prototype
,prototype
是一个对象,拥有Person
的constructor和sayHello
方法
使用Person
创建出一个实例person
此时person
上并没有sayHello
方法,但是person
的__proto__
指向Person
的prototype
,当在当前对象上找不到sayHello
方法时,js就会沿着__proto__
的指向向上寻找
后面我们用Object.create(person)
方法创建一个对象person2
,此时该对象是空对象,但是该对象的__proto__
指向person
,而person
的__proto__
又指向Person
的prototype
,因此person2
也可以调用sayHello
方法
这种沿着__proto__
指向一致向上寻找,便形成一个原型链,需要注意的时原型链的最顶端都是null
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
什么是闭包,有什么作用?
闭包是一种在函数内部创建的函数,它可以访问其包含函数(外部函数)的变量和参数,即使外部函数已经执行完毕。换句话说,闭包是一个函数加上该函数创建时所能访问的作用域,它可以"捕获"这个作用域中的变量,并在函数执行后继续保持对这些变量的访问能力。
闭包通常由以下两个主要部分组成:
- 内部函数(嵌套函数):在外部函数内部定义的函数,形成了闭包。
- 外部函数的作用域:内部函数可以访问和"捕获"外部函数的作用域中的变量和参数。
闭包的作用:
保护数据:
通过闭包,可以将一些数据限制在函数的作用域内,避免全局污染,只暴露必要的接口。保存状态:
闭包可以用于保存函数执行过程中的状态,例如计数器、缓存等,使得状态在函数调用之间得以保持。实现模块化:
通过闭包,可以实现类似于模块的封装,将一组相关的功能组织在一起,避免命名冲突和变量泄露。实现回调和异步操作:
JavaScript 中的回调函数和异步操作通常需要使用闭包来捕获外部函数的状态和数据。创建私有变量:
通过闭包可以模拟私有变量,使得只有内部函数能够访问和修改某些数据。
以下是一个闭包的简单示例:
function outerFunction() { let outerVariable = "I am from outer function"; function innerFunction() { console.log(outerVariable); // 内部函数访问了外部函数的变量 } return innerFunction; // 返回内部函数作为闭包 } let closure = outerFunction(); // 调用外部函数,返回内部函数 closure(); // 调用内部函数,输出 "I am from outer function"
在这个例子中,innerFunction 是一个闭包,它可以访问外部函数 outerFunction 中的变量 outerVariable。当我们调用外部函数并将其返回的内部函数赋值给 closure 变量后,closure 成为了一个保存了 outerVariable 的闭包,可以在之后的调用中继续访问这个变量。
JavaScript中的作用域是什么,有什么作用?
JavaScript 中的作用域(scope)是指变量和函数在代码中可访问的范围。作用域规定了在何处以及如何查找变量。作用域的概念在 JavaScript 中是非常重要的,因为它影响着变量的可见性和生命周期。
作用域的类型:
全局作用域:
在代码的最外层定义的变量和函数,可以在代码的任何位置被访问,称为全局作用域。局部作用域:
在函数内部定义的变量和函数,只能在函数内部被访问,称为局部作用域(也称为函数作用域)。
作用域的作用:
隔离变量:
作用域可以将变量限制在特定的代码块或函数内部,防止变量之间发生命名冲突。封装和信息隐藏:
通过使用函数作用域,可以将一些变量隐藏在函数内部,避免了外部代码直接访问和修改这些变量,实现了信息隐藏和封装。变量的生命周期:
作用域也影响了变量的生命周期,变量在进入作用域时被创建,在离开作用域时被销毁。这有助于内存管理和垃圾回收。
// 全局作用域 let globalVariable = "I am global"; function myFunction() { // 局部作用域 let localVariable = "I am local"; console.log(localVariable); // 可以访问局部变量 console.log(globalVariable); // 可以访问全局变量 } console.log(globalVariable); // 可以在全局范围内访问 // console.log(localVariable); // 无法在全局范围内访问局部变量
在这个示例中,globalVariable 是一个全局变量,可以在代码的任何地方访问。localVariable 是在函数内部定义的局部变量,只能在函数内部访问。
作用域帮助我们组织代码、封装功能、限制变量的可见性,并且有助于避免命名冲突,使得代码更加模块化和可维护。
如何理解JavaScript中的执行上下文?
执行上下文(Execution Context)是 JavaScript 中一个重要的概念,它是代码在执行过程中的环境,包含了当前代码执行所需的所有信息,如变量、函数、作用域等。每当 JavaScript 代码运行时,都会创建一个新的执行上下文。理解执行上下文对于理解代码的执行流程和作用域非常关键。
执行上下文可以分为三种类型:
全局执行上下文:
整个脚本的最外层环境,在代码执行之前被创建。在浏览器中,全局执行上下文通常是 window 对象。函数执行上下文:
每次调用函数时,都会创建一个新的函数执行上下文,用于管理函数内部的变量、参数和执行流程。Eval 函数执行上下文:
使用 eval 函数执行的代码会在一个特殊的执行上下文中运行。
执行上下文包含的重要信息:
变量环境(Variable Environment):
包含了变量和函数声明,以及外部环境的引用。在函数执行上下文中,这部分称为函数的“作用域链”。词法环境(Lexical Environment):
类似于变量环境,但是在函数执行上下文中,它会随着函数嵌套的改变而变化,支持闭包。this 值:
指向当前执行上下文的上下文对象。外部引用:
指向包含当前执行上下文的外部执行上下文。
执行上下文的生命周期:
创建阶段:
在进入代码块之前,执行上下文会被创建。这时会创建变量对象(VO),初始化函数参数、变量和函数声明。执行阶段:
在进入代码块时,代码会按顺序执行。在这个阶段,变量赋值和函数调用等操作会被执行。销毁阶段:
执行完代码后,执行上下文会被销毁,函数的局部变量等会被释放。
示例:
function greet(name) { var message = "Hello, " + name; console.log(message); } greet("Alice"); // 调用函数,创建函数执行上下文
在这个示例中,当调用 greet("Alice") 时,会创建一个新的函数执行上下文用于执行 greet 函数内部的代码。这个执行上下文包含了 name 参数和 message 变量,以及其他所需的信息。一旦函数执行完毕,这个执行上下文将会被销毁。
什么是Ajax,如何实现一个简单的Ajax请求
Ajax(Asynchronous JavaScript and XML) 是一种用于在不刷新整个页面的情况下,通过 JavaScript 在后台与服务器进行数据交换的技术。通过 Ajax,可以实现异步加载数据、动态更新页面内容,从而提升用户体验。
Ajax 的工作流程:
- 创建 XMLHttpRequest 对象:通过 JavaScript 创建一个 XMLHttpRequest 对象,用于在后台与服务器进行通信。
- 发送请求:使用 XMLHttpRequest 对象发送请求,可以是 GET 或 POST 请求,传递参数或数据给服务器。
- 接收响应:服务器处理请求后,返回数据或响应,JavaScript 通过监听事件来获取响应数据。
- 更新页面:根据接收到的响应数据,使用 JavaScript 动态更新页面内容,而不需要刷新整个页面。
以下是一个简单的 Ajax 请求的示例,使用原生的 XMLHttpRequest 对象:
<!DOCTYPE html> <html> <head> <title>Ajax Example</title> </head> <body> <button id="loadButton">Load Data</button> <div id="dataContainer"></div> <script> document.getElementById("loadButton").addEventListener("click", function() { let xhr = new XMLHttpRequest(); xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1", true); // 发送 GET 请求 xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { let response = JSON.parse(xhr.responseText); document.getElementById("dataContainer").textContent = response.title; } }; xhr.send(); // 发送请求 }); </script> </body> </html>
在这个示例中,点击 "Load Data" 按钮会触发一个 Ajax 请求,获取 https://jsonplaceholder.typicode.com/posts/1
地址返回的数据,并将其中的标题显示在页面上。XMLHttpRequest
对象用于创建和管理请求,onreadystatechange
事件监听响应状态的变化,readyState
表示请求状态,status
表示响应状态码,responseText
存储响应内容。
如何使用Promise封装Ajax请求
使用 Promise 封装 Ajax 请求可以让代码更具可读性和可维护性,同时还可以更好地处理异步操作的结果。以下是一个使用 Promise 封装 Ajax 请求的示例,使用原生的 XMLHttpRequest 对象:
function makeAjaxRequest(url, method) { return new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest(); xhr.open(method, url, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.responseText); // 请求成功,将响应数据传递给 resolve 函数 } else { reject(new Error("Request failed with status: " + xhr.status)); // 请求失败,将错误信息传递给 reject 函数 } } }; xhr.send(); }); } // 使用封装的函数 makeAjaxRequest("https://jsonplaceholder.typicode.com/posts/1", "GET") .then(function(response) { console.log("Response:", response); }) .catch(function(error) { console.error("Error:", error); });
在这个示例中,makeAjaxRequest 函数返回一个 Promise 对象,它将一个回调函数传递给 XMLHttpRequest 的 onreadystatechange 事件处理程序。当请求完成时,如果状态码为 200,则调用 resolve 函数并传递响应数据,否则调用 reject 函数并传递错误信息。
通过使用 Promise,你可以使用 .then() 方法处理成功的响应,使用 .catch() 方法处理失败的情况,使得代码逻辑更加清晰和可控。封装了 Promise 的 Ajax 请求可以更好地管理异步操作,使得代码更具可读性和可维护性。不过需要注意的是,现代的 Web 开发中,许多人更倾向于使用基于 Promise 的库(如 axios)来进行网络请求,因为它们提供了更便捷和强大的功能。
Ajax,axios,fetch之间有什么区别
Ajax、axios 和 fetch 都是用于在前端实现网络请求的工具或技术,但它们在用法和功能上有一些区别。
Ajax:
- Ajax(Asynchronous JavaScript and XML)是一种异步通信技术,使用原生的 JavaScript 和 XMLHttpRequest 对象来实现在不刷新整个页面的情况下与服务器交换数据。
- Ajax 可以用于发送各种类型的请求(GET、POST 等),并接收服务器返回的数据,然后通过 JavaScript 更新页面内容。
- 原生的 Ajax 代码较为繁琐,需要处理不同浏览器的兼容性。
axios:
- axios 是一个基于 Promise 的 HTTP 客户端,用于在浏览器和 Node.js 中发送 HTTP 请求。
- axios 提供了简洁的 API,并且处理了很多底层细节,如请求和响应的转换、拦截器、错误处理等。
- axios 可以在浏览器环境和 Node.js 环境中使用,支持请求取消和并发请求管理等功能。
fetch:
- fetch 是 Web API 中的一部分,是浏览器原生提供的一个用于发起网络请求的方法。
- fetch 提供了一种基于 Promise 的简单和现代的方式来发送请求和处理响应。
- 与 axios 不同,fetch 不会自动将请求失败(例如 HTTP 错误状态码)视为错误,需要手动检查响应状态。
区别总结:
- Ajax 是一种通信技术,主要通过 XMLHttpRequest 对象实现。
- axios 是一个第三方库,提供了基于 Promise 的 API,用于简化 HTTP 请求的处理。
- fetch 是浏览器原生提供的网络请求 API,也使用了 Promise,但处理响应状态的方式略有不同。
- axios 和 fetch 提供了更现代、更简洁的 API,且能处理一些附加功能,如拦截器、取消请求等。
JavaScript中的this是什么?
在 JavaScript 中,this 是一个特殊关键字,它在不同的上下文中指向不同的值。this 的值取决于代码在何处被调用,以及调用方式。
this 的指向可以分为以下几种情况:
全局上下文中的 this:
在全局上下文(没有嵌套在任何函数内部)中,this 指向全局对象,通常在浏览器环境中是 window 对象,在 Node.js 环境中是 global 对象。
函数中的 this:
- 在函数中,this 的值取决于函数是如何被调用的。常见的情况包括:
- 在函数中,直接使用 this,其值取决于函数的调用方式。
- 在函数中,作为对象的方法被调用,this 指向调用该方法的对象。
- 使用 call()、apply() 或 bind() 方法来明确指定函数执行时的 this。
箭头函数中的 this:
箭头函数没有自己的 this 值,它会继承外部函数的 this 值。箭头函数的 this 始终指向包含它的最近的非箭头函数的 this 值,即词法作用域的 this。
DOM 事件处理函数中的 this:
在处理 DOM 事件时,事件处理函数的 this 默认指向触发事件的 DOM 元素。但是,如果使用箭头函数作为事件处理函数,this 将保持其外部函数的 this 值。
示例:
console.log(this); // 全局上下文中,指向全局对象(浏览器环境中是 window) function sayHello() { console.log(this); // 在函数中,取决于调用方式 } const obj = { method: function() { console.log(this); // 在对象的方法中,指向调用方法的对象(obj) } }; const arrowFunction = () => { console.log(this); // 箭头函数中,继承外部函数的 this 值 }; sayHello(); // 函数中,取决于调用方式 obj.method(); // 对象的方法中,指向调用方法的对象(obj) arrowFunction(); // 箭头函数中,继承外部函数的 this 值
call,apply,bind函数如何使用,有何区别?
call、apply 和 bind 是 JavaScript 中用于显式设置函数执行上下文(this 值)的方法。它们的主要作用是在调用函数时,将指定的对象作为函数的上下文来执行函数。这些方法的区别在于参数传递的方式和执行时间。
call 方法:
- call 方法允许你将一个对象作为参数传递给函数,并将该对象作为函数的上下文(this 值)来调用函数。
- 除了第一个参数,后续的参数是传递给函数的实际参数。
- 函数会立即执行。
示例:
function greet(name) { console.log(`Hello, ${name}! I am ${this.name}.`); } const person = { name: "Alice" }; greet.call(person, "Bob"); // 输出:Hello, Bob! I am Alice.
apply 方法:
- apply 方法与 call 方法类似,但参数是以数组形式传递的。
- 函数会立即执行。
示例:
function greet(name) { console.log(`Hello, ${name}! I am ${this.name}.`); } const person = { name: "Alice" }; greet.apply(person, ["Bob"]); // 输出:Hello, Bob! I am Alice.
bind 方法:
- bind 方法返回一个新函数,该新函数的上下文被绑定到指定的对象,但不会立即执行。
- 返回的新函数可以稍后被调用,调用新函数时将使用绑定的上下文。
示例:
function greet(name) { console.log(`Hello, ${name}! I am ${this.name}.`); } const person = { name: "Alice" }; const boundGreet = greet.bind(person); boundGreet("Bob"); // 输出:Hello, Bob! I am Alice.
区别总结:
- call 和 apply 是用于立即执行函数并传递参数的方法,其中 apply 接受参数的形式是数组。
- bind 是用于创建一个新函数,该函数会在稍后的调用中使用绑定的上下文。
这些方法通常用于在特定上下文中执行函数,例如在事件处理程序、回调函数或特定环境中的函数调用。
2023前端面试题总结:JavaScript篇完整版(二):https://developer.aliyun.com/article/1415764