前端面试题手册
JavaScript 继承都有哪些方法?
在JavaScript中,继承是一个用来使一个类(子类)能够获取另一个类(父类)的属性和方法的机制。以下是在JavaScript中实现继承的几种方法:1. 原型链继承原型链继承是将子类的原型对象设置为父类的一个实例,从而实现继承。function Parent() { this.parentProperty = true;}Parent.prototype.getParentProperty = function() { return this.parentProperty;};function Child() { this.childProperty = false;}// 继承ParentChild.prototype = new Parent();var child = new Child();console.log(child.getParentProperty()); // true2. 构造函数继承构造函数继承通过在子类的构造函数中调用父类构造函数实现继承,并使用 .call()或 .apply()方法将子类的 this绑定到父类上。function Parent(name) { this.name = name;}function Child(name) { Parent.call(this, name);}var child = new Child('Alice');console.log(child.name); // Alice3. 组合继承(原型链 + 构造函数继承)组合继承结合了原型链继承和构造函数继承的优点,即子类的原型被设置为父类的一个实例,并且父类构造函数被用来增强子类实例。function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green'];}Parent.prototype.sayName = function() { console.log(this.name);};function Child(name, age) { Parent.call(this, name); this.age = age;}Child.prototype = new Parent();Child.prototype.constructor = Child;Child.prototype.sayAge = function() { console.log(this.age);};var child1 = new Child('Alice', 10);child1.colors.push('yellow');console.log(child1.name); // Aliceconsole.log(child1.age); // 10console.log(child1.colors); // ['red', 'blue', 'green', 'yellow']4. 原型式继承原型式继承是基于已有的对象创建新对象,使用 Object.create方法实现。var parent = { name: "Bob", getName: function() { return this.name; }};var child = Object.create(parent);child.name = "Alice";console.log(child.getName()); // Alice5. 寄生式继承寄生式继承创建一个封装继承过程的函数,这个函数在内部以某种方式增强对象然后返回。function createAnother(original) { var clone = Object.create(original); clone.sayHi = function() { console.log('Hi'); }; return clone;}var person = { name: 'Bob', getName: function() { return this.name; }};var anotherPerson = createAnother(person);anotherPerson.sayHi(); // Hi6. 寄生组合式继承寄生组合式继承通过使用寄生式继承来继承父类的原型,并将结果指定给子类的原型。function inheritPrototype(childObj, parentObj) { var prototype = Object.create(parentObj.prototype); prototype.constructor = childObj; childObj.prototype = prototype;}function Parent(name) { this.name = name;}Parent.prototype.sayName = function() { console.log(this.name);};function Child(name, age) { Parent.call(this, name); this.age = age;}inheritPrototype(Child, Parent);Child.prototype.sayAge = function() { console.log(this.age);};var child = new Child('Alice', 10);child.sayName(); // Alicechild.sayAge(); // 10
阅读 24·2024年6月24日 16:43
JavaScript 中 number 为什么会出现精度损失?应该怎样避免number的精度损失问题?
JavaScript 中的 number 类型是基于 IEEE 754 标准的双精度64位浮点数表示。这种表示方式导致了两类主要的精度问题:有限的位数: 64位中,有1位用于符号,11位用于表示指数,剩下的52位用于表示尾数(或分数)。这限制了可以精确表示的数字的范围和精度。当数字超出这个精确范围时,就会出现舍入误差。二进制浮点数的局限性: 并非所有的十进制小数都能被二进制系统精确地表示。例如,十进制的0.1在二进制中是一个无限循环的分数,就像十进制中的1/3不能精确表示一样。在二进制浮点数中,这样的十进制数会被近似为一个有限位数的二进制数,因此会有精度损失。例子:在 JavaScript 中计算 0.1 加 0.2 时,预期结果是 0.3,但实际结果往往是 0.30000000000000004,这展示了精度损失的问题。为了避免这种精度损失,可以使用以下策略:整数运算: 将浮点数转换为整数,进行运算后再转换回去。这适用于简单的加减乘除运算。 // 例子:使用整数运算来避免精度损失 let result = (0.1 * 10 + 0.2 * 10) / 10; // 结果为0.3使用第三方库: 为了处理更复杂的数学运算和避免精度损失,可以使用如 BigNumber.js 或 decimal.js 等第三方库,这些库提供了更为精确的数值计算能力。 // 使用 BigNumber.js 示例 BigNumber.config({ DECIMAL_PLACES: 10 }) let a = new BigNumber(0.1); let b = new BigNumber(0.2); let result = a.plus(b); // '0.3'内置 BigInt 类型: 对于整数运算,ES2020 引入了 BigInt 类型,它支持任意精度的整数。使用 BigInt 可以避免大整数计算中的精度损失,但它不适用于浮点数。 // 例子:使用 BigInt 进行大整数计算 let bigInt1 = BigInt("9007199254740993"); let bigInt2 = BigInt("1"); let result = bigInt1 + bigInt2; // 9007199254740994n总而言之,为了解决 JavaScript 中的 number 类型的精度问题,开发者需要根据实际情况选取适合的方法来保证数值的精确度。对于常规的小数点精度问题,转换为整数运算通常是最简单的解决办法;对于更复杂的场景,则可能需要使用第三方库或者 BigInt 类型。
阅读 31·2024年6月24日 16:43
改变 this 指向的方式都有哪些?
在JavaScript中改变 this指向的常见方式主要有以下几种:使用函数的 .bind()方法.bind()方法会创建一个新的函数,你可以传入一个对象来指定原函数中的 this。新函数的 this将被永久绑定到 .bind()的第一个参数上。 function greeting() { return `Hello, I'm ${this.name}`; } const person = { name: 'Alice' }; const boundGreeting = greeting.bind(person); console.log(boundGreeting()); // "Hello, I'm Alice"使用函数的 .call()和 .apply()方法.call()和 .apply()方法都是在特定的 this上调用函数,即可以直接指定 this的值。两者的区别在于如何传递函数的参数:.call()方法接受参数列表,而 .apply()方法接受一个包含多个参数的数组。 function introduction(name, profession) { console.log(`My name is ${name} and I am a ${profession}.`); } introduction.call(person, 'Alice', 'Engineer'); // "My name is Alice and I am a Engineer." introduction.apply(person, ['Alice', 'Engineer']); // "My name is Alice and I am a Engineer."箭头函数箭头函数不会创建自己的 this上下文,因此它的 this值继承自上一层作用域链。这是编写回调函数或闭包时常见的使用场景。 function Team(name) { this.name = name; this.members = []; } Team.prototype.addMember = function(name) { this.members.push(name); setTimeout(() => { console.log(`${name} has been added to the ${this.name} team.`); }, 1000); }; const team = new Team('Super Squad'); team.addMember('Hero'); // "Hero has been added to the Super Squad team." after 1 second在这个例子中,setTimeout中的箭头函数继承了 addMember方法的 this上下文。在回调函数中使用局部变量保存 this在 ES6 之前,由于函数的 this值在运行时确定,一个常见的模式是在闭包中用变量(通常是 self或 that)保存对外层 this的引用。 function Team(name) { this.name = name; this.members = []; var that = this; this.addMember = function(name) { that.members.push(name); setTimeout(function() { console.log(name + ' has been added to ' + that.name + ' team.'); }, 1000); }; } var team = new Team('Super Squad'); team.addMember('Hero'); // "Hero has been added to Super Squad team." after 1 second使用库或框架提供的功能一些JavaScript库和框架提供了自己的方法来绑定或者定义 this的上下文,比如在React组件的事件处理中,你可能会使用类似 .bind()的方法。在类中使用箭头函数定义方法在ES6类中,你可以使用箭头函数定义类的方法,这样就能确保方法内部的 this绑定到类的实例。 class Button { constructor(label) { this.label = label; } handleClick = () => { console.log(`Clicked on: ${this.label}`); } } const button = new Button('Save'); const btnElement = document.createElement('button'); btnElement.textContent = button.label; btnElement.addEventListener('click', button.handleClick); // 点击按钮时,会正确打印 "Clicked on: Save"以上就是JavaScript中改变 this指向的主要方式。
阅读 7·2024年6月24日 16:43
什么是 base64 编码方式?它有什么作用?
Base64是一种基于64个可打印字符来表示二进制数据的编码方法。这种编码方式设计用来确保二进制数据在编码过程中能够通过不同的媒介,特别是那些只支持ASCII文本的媒介,不会因为字符解读错误而破坏。Base64编码方式的作用包括:数据编码:将二进制数据转换成ASCII字符串,这样数据就可以在文本环境下安全传输,比如通过电子邮件或者XML文件。提升兼容性:某些系统不支持所有的二进制数据或特殊字符,Base64编码后的数据可以在这些系统中无障碍传输。打印友好:Base64编码后的字符串包含的是可打印字符,方便打印和查看。Base64编码规则非常简单,基本过程如下:将原始二进制数据的每个字节分成6位一组,如果最后一组不足6位,则用0填充。对照Base64索引表将这些6位的组合转换成相应的字符。Base64索引表包含了大小写英文字母各26个,加上10个数字和+、/两个符号,共64个字符。如果编码后的字符数不是4的倍数,则用=字符填充,以确保最终的输出字符数是4的倍数。举个例子,如果我们要编码单词"Man"为Base64:原始ASCII码是"M"=77, "a"=97, "n"=110二进制表示为:01001101 01100001 01101110划分成6位一组:010011 010110 000101 101110对照Base64索引表转换:T W F u因此,"Man"这个单词用Base64编码后是"TWFu"。
阅读 23·2024年6月24日 16:43
setTimeout 有什么缺点?setTimeout 和 requestAnimationFrame 之间有什么区别?
setTimeout 的缺点setTimeout 函数是 Web API 的一部分,它可以在指定的毫秒数后执行一个函数或指定的代码。然而,setTimeout 有几个缺点:不精确的时间控制:setTimeout 并不能保证在指定时间后立即执行,因为它受到 JavaScript 事件循环的影响。如果事件队列中有其他任务,setTimeout 的回调可能会延迟执行。性能问题:使用 setTimeout 进行重复的或高频的任务(例如动画)可能会导致性能问题。因为它不会考虑浏览器的绘制帧。这可能会导致动画不流畅或者页面重绘。多个定时器的管理:如果页面上有多个 setTimeout 定时器,管理和清除这些定时器可能会变得复杂。资源消耗:即使浏览器窗口或页面不在前台时,setTimeout 也会继续执行,这可能会导致不必要的 CPU 和电力消耗。setTimeout 与 requestAnimationFrame 的区别setTimeout 和 requestAnimationFrame(简称 rAF)都可以用于延迟执行代码,但它们的用途和行为有显著的区别:目的:setTimeout 用于在设定的时间后执行一次回调函数。requestAnimationFrame 主要用于动画,它告诉浏览器在下次重绘之前执行一个函数,以便动画可以平滑地按照屏幕的刷新率运行。执行时机:setTimeout 的回调执行时间不一定与浏览器的绘制帧同步。requestAnimationFrame 的回调会在浏览器绘制下一帧之前执行,这通常意味着回调以 60 次/秒的频率执行(或者与显示器的刷新率相匹配)。性能:setTimeout 可能会导致掉帧,因为它不考虑浏览器的帧率。requestAnimationFrame 会与浏览器的帧率同步,减少掉帧的情况,因此动画更平滑,性能也更优。节能:setTimeout 在后台标签页或隐藏的 iframe 中仍然会运行,可能导致不必要的资源消耗。requestAnimationFrame 在页面不可见时会自动暂停,从而节省资源。使用场景:setTimeout 适用于不需要与帧率同步的一次性或非频繁的延迟任务。requestAnimationFrame 适用于需要高性能动画的场景,例如游戏或界面动效。
阅读 51·2024年6月24日 16:43
let 块作用域是怎么实现的?
let 关键字在JavaScript中被引入是为了提供块作用域(block scope)的功能。块作用域意味着由 let 声明的变量仅在声明它们的代码块内部是可见的。代码块是被花括号 {} 包围的一段代码,例如在 if 语句、for 和 while 循环以及函数定义中都会用到代码块。在ES6之前,JavaScript主要依赖的是函数作用域(function scope),由 var 关键字声明的变量要么是全局的,要么是在函数内部局部的。这种设计有时会导致意料之外的问题,特别是在循环中。下面是一个使用 let 的例子来说明块作用域是如何工作的:function runLoop() { for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100 * i); }}runLoop();在这个例子中,变量 i 是用 let 在 for 循环的块中声明的。这意味着每次循环迭代时,变量 i 都是一个新的变量,并且它被限制在这个循环的块作用域中。所以当 setTimeout 的回调函数执行时,它能够访问到循环迭代时对应的 i 的值。如果我们用 var 替换掉 let,结果将会不同:function runLoop() { for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100 * i); }}runLoop();在这个例子中,由于 var 声明的变量 i 是函数作用域的,当 setTimeout 的回调函数执行时,它会打印出循环结束后变量 i 的最终值,即5,会打印五次5,而不是0到4。总结来说,let 关键字允许开发者在更细粒度的级别控制变量的作用域。这样做提高了代码的可读性和可维护性,并且减少了由于作用域导致的常见错误。
阅读 28·2024年6月24日 16:43
some、every、find、filter、map、forEach 有什么区别?
JavaScript 中的 some、every、find、filter、map 和 forEach 都是数组的方法,它们各自有不同的用途。somesome 方法用于检查数组中是否至少有一个元素满足提供的测试函数。如果满足则返回 true,否则返回 false。这个方法对于检查数组是否包含至少一个符合条件的元素很有用。例子:const hasNegativeNumbers = [1, 2, 3, -1, 4].some(num => num < 0); // trueeveryevery 方法用来检查数组中的所有元素是否都满足提供的测试函数。如果全部满足则返回 true,否则返回 false。这个方法适用于验证数组所有元素是否符合某个条件。例子:const allPositiveNumbers = [1, 2, 3].every(num => num > 0); // truefindfind 方法用于找到数组中第一个满足提供的测试函数的元素。如果找到了这样的元素,find 就会返回这个元素,否则返回 undefined。例子:const firstNegativeNumber = [1, 2, 3, -1, 4].find(num => num < 0); // -1filterfilter 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。这个方法用于根据条件筛选数组中的元素。例子:const negativeNumbers = [1, 2, 3, -1, -2, 4].filter(num => num < 0); // [-1, -2]mapmap 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后的返回值。这个方法用于转换数组中的每个元素。例子:const squares = [1, 2, 3, 4].map(num => num * num); // [1, 4, 9, 16]forEachforEach 方法对数组的每个元素执行一次提供的函数,但它不返回任何值(即 undefined)。这只是一个简单的遍历数组的办法,通常用于执行副作用(如打印日志、更新UI等)。例子:[1, 2, 3, 4].forEach(num => console.log(num)); // 输出 1 2 3 4,但没有返回值每一个这些方法都有其特定的用途,选择哪个取决于您要解决的特定问题。
阅读 29·2024年6月24日 16:43
ES5 和 ES6 有什么区别
ES5(即ECMAScript 5)和ES6(也称为ECMAScript 2015或ECMAScript 6)是JavaScript语言的两个版本,它们之间有许多重要的区别。ES6引入了一系列新特性和语法改进,使得编程更加简洁和强大。以下是一些主要的区别:1. let 和 constES6引入了 let和 const关键字,用于声明变量。let提供了块级作用域,比ES5中的 var提供了更好的控制,尤其是在循环中。const用于声明常量,其值在设置之后不能改变。例子:在一个循环中使用 let可以避免常见的闭包问题。for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000);}// 输出:0, 1, 2, 3, 4 (正确的顺序)2. 箭头函数(Arrow Functions)ES6引入了箭头函数,这是一种更简洁的函数写法,同时它也没有自己的 this,arguments,super或 new.target绑定。例子:// ES5var add = function(a, b) { return a + b;};// ES6const add = (a, b) => a + b;3. 模板字符串(Template Literals)在ES6中,模板字符串提供了一种构建字符串的新方法,可以使用反引号( `)来定义,它支持多行字符串和字符串插值。例子:// ES5var name = "World";var greeting = "Hello, " + name + "!";// ES6const name = "World";const greeting = `Hello, ${name}!`;4. 类(Classes)ES6引入了类的概念,这是一种使用原型继承的语法糖。它使创建对象和继承更加直观和方便。例子:// ES6class Person { constructor(name) { this.name = name; } greet() { return `Hello, ${this.name}!`; }}const person = new Person('Jane');console.log(person.greet()); // "Hello, Jane!"5. 默认参数值ES6允许函数参数有默认值,这在ES5中通常需要在函数体内部进行处理。例子:// ES6function greet(name = "World") { return `Hello, ${name}!`;}console.log(greet()); // "Hello, World!"console.log(greet("Jane")); // "Hello, Jane!"6. 解构赋值(Destructuring Assignment)ES6引入了解构赋值,它允许在单个语句中从数组或对象中提取数据,并设置到新的变量中。例子:// ES6const [a, b] = [1, 2];const { firstName, lastName } = { firstName: "John", lastName: "Doe" };console.log(a); // 1console.log(firstName); // "John"7. 模块导入和导出ES6标准化了模块系统,使用 import和 export语句来导入和导出模块成员。例子:// ES6// file: math.jsexport const add = (a, b) => a + b;// file: main.jsimport { add } from './math';console.log(add(2, 3)); // 58. Promises 和异步编程ES6引入了Promise作为处理异步操作的一种机制,它比ES5中的回调函数更具可读性和效率。
阅读 28·2024年6月24日 16:43
ES6 中的 Map 和原生的对象有什么区别?
在 ES6 中,Map 是一种新的数据结构,它提供了一些原生对象(如普通的 JavaScript 对象)所不具备的特性。以下是 Map 和原生对象之间一些主要的区别:键的类型:Map:可以使用任何类型的值(包括对象或原始值)作为键。对象:通常只能使用字符串或者 Symbol 作为键。虽然现代JavaScript引擎会自动将非字符串的键转换为字符串,但这可能导致键的冲突和预期之外的行为。键的顺序:Map:键值对是有序的,Map 对象遍历时会根据元素的插入顺序进行。对象:在 ES2015 之前,对象的属性没有特定的顺序;但从 ES2015 开始,对象的属性遍历顺序是根据属性被添加到对象的顺序(对于字符串键)和整数键的大小来确定的,非整数键则按照创建顺序排列。大小可获取:Map:可以直接获取到 Map 的大小,使用 map.size 属性。对象:通常需要手动计算属性的数量,例如通过 Object.keys(obj).length。性能:Map:在频繁添加和删除键值对的场景下,Map 通常提供更优的性能。特别是当涉及到大量键值对时,Map 的性能通常更稳定。对象:当作为少量属性的集合时,原生对象也可能表现出良好的性能。默认键:Map:不包含默认键,只包含显式插入的键。对象:原型链上的属性和方法可以被继承,对象默认会含有诸如 toString 或 hasOwnProperty 这样的方法,这可能会在某些使用场景中造成问题。迭代:Map:Map 对象可以直接被迭代,提供了几个迭代方法,包括 map.keys()、map.values() 和 map.entries(),以及 map.forEach() 方法。对象:对象的属性需要使用 for...in 循环或 Object.keys()、Object.values()、Object.entries() 加上 forEach 方法等进行迭代。序列化:Map:Map 对象不能直接使用 JSON.stringify 进行序列化。对象:对象可以直接被序列化为 JSON 字符串。例如,如果我们需要一个键值对集合来记录用户的唯一标识符(这些标识符可能是数字、字符串、甚至是对象),并且希望保持插入顺序,那么 Map 就特别适合这种用例。使用 Map 我们可以这样实现:let userRoles = new Map();let user1 = { name: "Alice" };let user2 = { name: "Bob" };// 添加用户角色userRoles.set(user1, 'admin');userRoles.set(user2, 'editor');// 获取Map的大小console.log(userRoles.size); // 2// 按插入顺序遍历用户角色for (let [user, role] of userRoles.entries()) { console.log(`${user.name}: ${role}`);}在这个例子中,我们使用对象 user1 和 user2 作为键,这在普通的对象中是无法做到的,因为对象的键会被转换为字符串。
阅读 36·2024年6月24日 16:43
es6 类继承中 super 的作用
ES6中,super关键字在类继承中扮演着非常重要的角色。它有两个主要的作用:在子类构造函数中调用父类的构造函数:在使用ES6类继承时,子类的构造函数需要调用父类的构造函数,这是通过super()实现的。这使得子类能够继承父类的属性。如果不调用super(),则子类的实例将无法正确构建,因为父类的一些初始化代码不会被执行。例如,假设我们有一个Person类和一个继承自Person的Student类: class Person { constructor(name) { this.name = name; } } class Student extends Person { constructor(name, studentID) { super(name); // 调用父类的构造函数来初始化父类中定义的属性 this.studentID = studentID; } } let student = new Student('Alice', '12345'); console.log(student.name); // 输出: Alice在这个例子中,super(name)调用了Person类的构造函数,初始化了name属性。在子类的方法中调用父类的方法:super也可以用来在子类中调用父类的方法。这对于扩展和重写父类行为非常有用。在子类的方法中,你可以通过super.methodName()的方式调用父类的方法。例如: class Person { greet() { console.log(`Hello, my name is ${this.name}.`); } } class Student extends Person { study() { console.log(`${this.name} is studying with student ID ${this.studentID}.`); } greet() { super.greet(); // 调用父类的greet方法 this.study(); // 然后调用子类的study方法 } } let student = new Student('Alice', '12345'); student.greet(); // 输出: // Hello, my name is Alice. // Alice is studying with student ID 12345.在这个例子中,我们重写了Student类的greet方法,在其中首先通过super.greet()调用了父类Person中的greet方法,然后调用了Student自己的study方法,这样就可以保留父类的行为的同时扩展新的行为。综上所述,super在ES6类继承中是极为重要的,它允许子类构造函数和方法访问和调用父类的构造函数和方法。
阅读 25·2024年6月24日 16:43