JS初级高频面试真题汇总

166 阅读27分钟

本片主要介绍高频面试真题,帮助大家进一步了解大厂在一、二面中很有可能出现的问题。进一步巩固一下,关于前端基础知识的相关记忆。

1、var和let const的区别

  1. var声明的变量是函数作用域或全局作用域,而let和const声明的变量是块级作用域的。
  2. var声明的变量可以被重复声明,但let和const不允许重复声明。
  3. 在变量声明前使用var声明的变量,其值为undefined,而使用let和const声明的变量会抛出ReferrenceError。
  4. let声明的变量可以被重新赋值,而const声明的变量不能被重新赋值,但是const声明的对象或数组中的属性可以被修改。

下面是一些示例代码:

// var 声明变量
var a = 1;
function foo() {
  var b = 2;
  if (true) {
    var c = 3;
  }
  console.log(a, b, c); // 1, 2, 3
}
foo();
console.log(a, b, c); // 1, undefined, 3

// let 声明变量
let x = 1;
function bar() {
  let y = 2;
  if (true) {
    let z = 3;
  }
  console.log(x, y, z); // 1, 2, ReferenceError
}
bar();
console.log(x, y, z); // 1, undefined, ReferenceError

// const 声明变量
const obj = { a: 1 };
obj.a = 2; // 可以修改对象的属性
console.log(obj.a); // 2
const arr = [1, 2, 3];
arr.push(4); // 可以修改数组
console.log(arr); // [1, 2, 3, 4]
const b = 1;
b = 2; // 抛出 TypeError

2、typeof返回哪些类型

JavaScript中的typeof运算符可以返回变量值是以下类型:

  • “undefined”:未定义
  • “boolean”:布尔值
  • “number”:数字
  • “string”:字符串
  • “bigint”:大整数
  • “symbol”:符号
  • “object”:对象(包括数组和null)或函数
  • “function”:函数

需要注意的是,typeof null 返回 "object",这是一个已知的历史遗留问题。

3、列举强制类型转换和隐式类型转换

在 JavaScript 中,类型转换分为强制类型转换和隐式类型转换两种。

强制类型转换是通过代码显式地调用类型转换函数或操作符来完成的。例如,使用 Number() 函数将一个字符串转换为数字类型,使用 String() 函数将一个数字转换为字符串类型,使用 Boolean() 函数将一个值转换为布尔类型。

隐式类型转换是在代码执行期间自动完成的,通常发生在运算符应用于不同类型的值时。例如,使用 + 运算符将一个数字和一个字符串相加时,JavaScript 会将字符串隐式转换为数字类型,然后进行加法运算。又如,在条件语句中,JavaScript 会将非布尔类型的值隐式转换为布尔类型,然后进行判断。

以下是一些常见的强制类型转换和隐式类型转换的例子:

强制类型转换:

  • Number("123") 将字符串 "123" 转换为数字类型 123
  • String(123) 将数字 123 转换为字符串类型 "123"
  • Boolean(0) 将数字 0 转换为布尔类型 false

隐式类型转换:

  • "123" + 456 将字符串 "123" 隐式转换为数字类型 123,然后进行加法运算得到结果 579
  • if ("hello") { ... } 将字符串 "hello" 隐式转换为布尔类型 true,然后执行条件语句中的代码块。
  • null == undefinednull 隐式转换为 undefined,然后进行相等性比较得到结果 true

需要注意的是,在进行类型转换时,JavaScript 会根据一定的规则进行转换,如果不了解这些规则,可能会导致一些意外的结果。因此,在进行类型转换时,需要仔细思考和测试代码,以确保得到正确的结果。

4、 手写深度比较,模拟lodash isEqual

function isEqual(value, other) {
  // 判断两个参数是否相等
  if (value === other) return true;

  // 判断参数类型是否相等
  if (typeof value !== typeof other) return false;

  // 判断参数是否是 null 或 undefined
  if (value == null || other == null) return false;

  // 判断参数是否是基本数据类型
  if (typeof value !== 'object' && typeof other !== 'object') return false;

  // 判断参数是否是数组
  if (Array.isArray(value) !== Array.isArray(other)) return false;

  // 判断参数是否是日期对象
  if (value instanceof Date && other instanceof Date) {
    return value.getTime() === other.getTime();
  }

  // 判断参数是否是 RegExp 对象
  if (value instanceof RegExp && other instanceof RegExp) {
    return value.toString() === other.toString();
  }

  // 判断参数是否是对象
  if (typeof value === 'object' && typeof other === 'object') {
    const keys1 = Object.keys(value);
    const keys2 = Object.keys(other);

    // 判断对象的属性数量是否相等
    if (keys1.length !== keys2.length) return false;

    // 判断对象的属性是否相等
    for (let key of keys1) {
      if (!keys2.includes(key) || !isEqual(value[key], other[key])) {
        return false;
      }
    }

    return true;
  }

  return false;
}

5、split()和join()区别

split()join()都是字符串的方法,但它们的作用是不同的。

split()方法可以将一个字符串分割成一个字符串数组,根据指定的分隔符来切割字符串。例如:

const str = "apple,banana,orange";
const arr = str.split(",");
console.log(arr); // ["apple", "banana", "orange"]

join()方法则是将一个数组中的所有元素连接成一个字符串,可以指定连接符。例如:

const arr = ["apple", "banana", "orange"];
const str = arr.join(",");
console.log(str); // "apple,banana,orange"

需要注意的是,split()方法返回的是一个数组,而join()方法返回的是一个字符串。

6、数组的pop push unshift shift分别做什么

这四个方法都是用来操作数组的,它们的作用分别是:

  • push(): 向数组的末尾添加一个或多个元素,并返回新的长度。
  • pop(): 删除数组的最后一个元素,并返回该元素的值。
  • unshift(): 向数组的开头添加一个或多个元素,并返回新的长度。
  • shift(): 删除数组的第一个元素,并返回该元素的值。

例如:

const arr = [1, 2, 3];
arr.push(4); // [1, 2, 3, 4]
arr.pop();   // [1, 2, 3]
arr.unshift(0); // [0, 1, 2, 3]
arr.shift(); // [1, 2, 3]

需要注意的是,这四个方法都会改变原数组,如果不想改变原数组,可以使用slice()方法来创建一个新的数组。

7、数组slice和splice的区别

slice()splice()都是数组的方法,它们的作用是不同的。

slice()方法用于从数组中提取指定的一段元素,返回一个新数组,不会改变原数组。slice()方法可以接受两个参数,第一个参数是起始位置(包括该位置),第二个参数是结束位置(不包括该位置),如果不指定第二个参数,则会提取从起始位置到数组末尾的所有元素。例如:

const arr = [1, 2, 3, 4, 5];
const newArr = arr.slice(1, 4); // [2, 3, 4]
console.log(arr); // [1, 2, 3, 4, 5]

splice()方法用于向数组中添加或删除元素,会改变原数组。splice()方法可以接受三个参数,第一个参数是起始位置(包括该位置),第二个参数是要删除的元素个数,第三个参数是要添加到数组中的元素。例如:

const arr = [1, 2, 3, 4, 5];
arr.splice(1, 2, "a", "b"); // [2, 3]
console.log(arr); // [1, "a", "b", 4, 5]

需要注意的是,splice()方法会改变原数组,如果不想改变原数组,可以使用slice()方法来创建一个新的数组。

8、[10,20,30].map(parseInt)返回结果是什么

如果你使用了正确的函数名,即[10,20,30].map(parseInt),那么它会返回一个数组,数组中的元素是将原数组中的每个元素作为参数传递给parseInt函数后得到的结果。但是,由于map方法在调用parseInt函数时会传递两个参数,第一个参数是数组元素的值,第二个参数是数组元素的索引,因此会导致结果不是我们期望的。具体来说,parseInt函数会将第二个参数(索引值)作为解析数字的进制,因此最终的结果会出现错误。例如,parseInt("10", 0)会返回10,而parseInt("20", 1)parseInt("30", 2)都会返回NaN。因此,建议在使用map方法时,传递一个函数作为参数,而不是直接传递parseInt函数。

结果是: [10, NaN, NaN]

9、ajax请求get和post的区别

GET和POST是HTTP协议中常用的两种请求方法,它们的主要区别如下:

  1. GET请求会将请求参数放在URL的查询字符串中,而POST请求会将请求参数放在请求体中。因此,GET请求的请求参数在URL中可以直接看到,而POST请求的请求参数在URL中不可见。

  2. GET请求的请求参数有长度限制,通常不能超过2048个字符,而POST请求的请求参数没有长度限制。

  3. GET请求是幂等的,即多次请求会得到相同的结果,不会产生副作用。而POST请求不是幂等的,即多次请求可能会产生不同的结果,会产生副作用。

  4. GET请求通常用于获取资源,POST请求通常用于提交数据。

综上所述,如果请求的数据较少且不敏感,可以使用GET请求;如果请求的数据较多或包含敏感信息,应该使用POST请求。

10、函数call和apply的区别

callapply都是 JavaScript 中的函数方法,它们的作用是改变函数中的this指向。

callapply的区别在于它们传递参数的方式不同:

  • call方法:第一个参数为函数中的this指向,后面的参数为传递给函数的参数,需要一个一个列举出来。
  • apply方法:第一个参数为函数中的this指向,第二个参数为传递给函数的参数,需要以数组的形式传递。

举个例子:

const obj = {
  name: 'Lucy'
}

function sayHello(age) {
  console.log(`Hello, I am ${this.name}, ${age} years old.`);
}

sayHello.call(obj, 18); // output: Hello, I am Lucy, 18 years old.
sayHello.apply(obj, [18]); // output: Hello, I am Lucy, 18 years old.

在上面的例子中,callapply都将函数中的this指向了obj对象,并将age参数传递给了函数。但是,call方法将参数一个一个传递,而apply方法将参数以数组的形式传递。

11、事件代理(委托)是什么

事件代理(也叫事件委托)是一种常用的 JavaScript 事件处理机制,它利用事件冒泡机制,将事件处理程序添加到父元素上,从而避免在子元素上添加大量的事件处理程序,提高了性能。

事件代理的核心思想是将事件绑定到父元素上,然后通过事件冒泡机制来判断事件源是否是我们需要处理的元素,从而执行相应的操作。这样一来,我们就可以通过父元素来代理它的子元素的事件处理,从而避免在子元素上添加大量的事件处理程序。

举个例子,假设我们有一个ul元素,其中包含多个li元素,我们需要为每个li元素添加点击事件,可以使用事件代理的方式:

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  <li>item 4</li>
</ul>
const list = document.getElementById('list');

list.addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    console.log('clicked item:', event.target.textContent);
  }
});

在上面的例子中,我们给ul元素添加了一个点击事件处理程序,当点击了ul元素中的任意一个li元素时,事件会冒泡到ul元素,此时我们通过判断事件源是否是li元素来执行相应的操作。

使用事件代理的好处是可以减少事件处理程序的数量,提高性能,同时也方便了动态添加和删除元素的操作。

12、闭包是什么,有什么特性? 有什么负面影响

闭包是指一个函数能够访问其外部函数作用域中的变量,并将这些变量保存在内存中,即使外部函数已经执行完毕,这些变量依然可以被内部函数访问和操作。闭包是JavaScript中的一种重要的特性,它可以帮助我们实现许多高级的功能,如模块化开发、函数柯里化、事件处理等。

闭包的特性包括:

  1. 内部函数可以访问外部函数的变量和参数。
  2. 外部函数的变量和参数不会被垃圾回收机制回收,因为内部函数仍然在使用它们。
  3. 多个内部函数可以共享同一个外部函数的变量和参数。

闭包的负面影响包括:

  1. 内存泄漏:如果闭包中的变量没有被正确地释放,会造成内存泄漏,导致程序运行速度变慢。
  2. 性能问题:由于闭包会占用内存,所以在使用闭包时需要注意内存的使用情况,以避免程序运行变慢。
  3. 安全问题:如果闭包中的变量被恶意修改,可能会导致程序出现安全漏洞,因此在使用闭包时需要注意安全问题。

13、如何阻止事件冒泡和默认行为

在JavaScript中,可以使用以下两种方式来阻止事件冒泡和默认行为:

  1. 阻止事件冒泡:

使用事件对象的stopPropagation()方法可以阻止事件冒泡。例如,在点击一个按钮时,如果不希望点击事件冒泡到父元素,可以使用以下代码:

document.getElementById("button").addEventListener("click", function(event){
  event.stopPropagation();
});
  1. 阻止默认行为:

使用事件对象的preventDefault()方法可以阻止事件的默认行为。例如,在点击一个链接时,如果不希望链接跳转到目标页面,可以使用以下代码:

document.getElementById("link").addEventListener("click", function(event){
  event.preventDefault();
});

需要注意的是,阻止事件冒泡和阻止默认行为的方式都需要在事件处理函数中使用,并且需要在事件处理函数中的最开始使用,否则可能会导致无法生效。

14、查找、添加、删除、移动DOM节点的方法

在JavaScript中,可以使用以下方法来查找、添加、删除、移动DOM节点:

  1. 查找节点:
  • getElementById(id):根据id属性查找节点。
  • getElementsByTagName(tagName):根据标签名查找节点,返回一个HTMLCollection对象。
  • getElementsByClassName(className):根据class属性查找节点,返回一个HTMLCollection对象。
  • querySelector(selector):根据CSS选择器查找节点,返回第一个匹配的节点。
  • querySelectorAll(selector):根据CSS选择器查找节点,返回所有匹配的节点,返回一个NodeList对象。
  1. 添加节点:
  • createElement(tagName):创建一个指定标签名的元素节点。
  • createTextNode(text):创建一个包含指定文本的文本节点。
  • appendChild(node):将指定节点添加到父节点的子节点列表的末尾。
  • insertBefore(newNode, referenceNode):将指定节点插入到参考节点之前。
  1. 删除节点:
  • removeChild(node):从父节点中删除指定节点。
  • parentNode.removeChild(node):从父节点中删除指定节点。
  1. 移动节点:
  • appendChild(node):将指定节点添加到父节点的子节点列表的末尾。
  • insertBefore(newNode, referenceNode):将指定节点插入到参考节点之前。
  • replaceChild(newNode, oldNode):用指定节点替换指定节点。

需要注意的是,以上方法需要在操作DOM节点之前先获取到对应的节点,可以使用查找节点的方法来获取。

15、如何减少DOM操作

DOM操作是非常耗费性能的,因为每次操作都会引起浏览器的重排和重绘,所以我们应该尽量减少DOM操作。下面是一些减少DOM操作的方法:

  1. 缓存DOM元素:在JavaScript中,每次使用一个DOM元素时,都需要重新查询它。所以我们可以将DOM元素缓存到一个变量中,这样就可以避免多次查询。

  2. 使用文档片段:如果需要添加多个DOM元素到页面中,可以先将它们添加到文档片段中,然后再将文档片段一次性添加到页面中。这样可以减少DOM操作次数。

  3. 批量修改样式:如果需要修改多个DOM元素的样式,可以将它们的样式放到一个类中,然后通过修改这个类来批量修改样式。

  4. 避免频繁的重排和重绘:当需要对DOM元素进行多次修改时,可以将这些修改操作放到一个函数中,然后将这个函数放到requestAnimationFrame中执行。这样可以将多次重排和重绘合并成一次。

  5. 使用事件委托:事件委托是将事件绑定到父元素上,然后通过事件冒泡机制来处理子元素的事件。这样可以减少事件绑定的次数,提高性能。

  6. 使用虚拟DOM:虚拟DOM是一种将DOM操作抽象成JavaScript对象的技术。在修改虚拟DOM时,不会立即对页面进行修改,而是先将修改操作保存到虚拟DOM中,然后再将虚拟DOM和真实DOM进行比较,最后只对需要修改的DOM进行修改,从而减少DOM操作的次数。

16、解释jsonp的原理,为何它不是真正的ajax

JSONP(JSON with Padding)是一种跨域数据请求技术,它利用了HTML中script标签的特性来实现跨域请求数据。JSONP的原理是通过动态创建script标签,将请求的数据作为参数传递给一个回调函数,然后服务器将数据包装在回调函数中返回给客户端,客户端再通过回调函数来处理返回的数据。

JSONP的工作流程如下:

  1. 客户端创建一个script标签,并将请求的URL设置为script的src属性。

  2. 服务器接收到请求后,将请求的数据包装在一个回调函数中,并将回调函数的名称作为参数传递给客户端。

  3. 客户端接收到服务器返回的数据后,会自动执行回调函数,并将返回的数据作为参数传递给回调函数。

  4. 回调函数处理返回的数据,并将处理后的数据显示在页面上。

JSONP不是真正的AJAX,因为它并不使用XMLHttpRequest对象来发送请求,而是利用script标签的特性来实现跨域请求数据。此外,JSONP只支持GET请求,无法支持POST请求等其他HTTP请求方式。

虽然JSONP的使用非常方便,但是它存在一些安全隐患,因为服务器返回的数据可以执行任意的JavaScript代码,可能会被恶意攻击者利用来进行XSS攻击。为了避免这种情况的发生,服务器需要对返回的数据进行严格的过滤和验证。

17、document load 和ready的区别

document loadready都是页面加载完成后执行JavaScript代码的事件。

document load事件是在页面的所有资源(包括图片、CSS、JS等)加载完成后才会触发,此时document对象已经完全加载并解析完毕。

ready事件则是在DOM结构加载完成后就会触发,此时页面的资源可能还没有完全加载完成。ready事件通常使用jQuery库中的$(document).ready()方法来绑定。

因此,如果需要在DOM结构加载完成后就执行一些JavaScript代码,可以使用ready事件;如果需要在所有资源都加载完成后再执行JavaScript代码,可以使用document load事件。

18、==和===的不同

=====都是JavaScript中的比较运算符,用于比较两个值是否相等。

==比较的是值是否相等,它会自动进行类型转换。如果比较的两个值类型不同,会先将其中一个值转换为另一个值的类型,然后再进行比较。比如,1 == '1'会返回true,因为它们的值相同,而且JavaScript会将字符串类型的'1'自动转换为数字类型的1

===比较的是值和类型是否都相等,不会进行类型转换。如果比较的两个值类型不同,直接返回false。比如,1 === '1'会返回false,因为它们的类型不同。

因此,建议在比较值时使用===,因为它可以避免类型转换带来的意外结果。如果需要进行类型转换,可以使用其他方法,如parseInt()parseFloat()等。

19、函数声明和函数表达式的区别

JavaScript中有两种定义函数的方式:函数声明和函数表达式。

函数声明是指使用function关键字定义的函数,它可以在任何地方声明,并且会被提升到当前作用域的顶部。例如:

function add(a, b) {
  return a + b;
}

函数表达式是指将函数定义为一个变量的值,它只能在赋值语句之后调用。例如:

var add = function(a, b) {
  return a + b;
};

函数表达式可以是匿名的,也可以有一个名称。如果有名称,它只能在函数内部使用。例如:

var add = function sum(a, b) {
  return a + b;
};

函数表达式的好处是可以将函数作为参数传递给其他函数,或者将函数赋值给其他变量。它还可以用来创建闭包和立即执行函数表达式(IIFE)等高级用法。

总的来说,函数声明和函数表达式都可以用来定义函数,但它们的作用域和调用方式有所不同。在实际开发中,需要根据具体的情况选择使用哪种方式来定义函数。

20、new Object()和Object.creat()的区别

new Object()Object.create()都可以用来创建一个新的对象,但它们的实现方式和用途有所不同。

new Object()是通过调用Object构造函数来创建一个新的对象。它会创建一个空对象,并将该对象的原型设置为Object.prototype。可以通过在构造函数中传入参数来设置新对象的属性和方法。

例如:

const obj = new Object();
obj.name = 'John';
obj.sayHello = function() {
  console.log('Hello!');
};

Object.create()则是通过传入一个原型对象来创建一个新的对象。它会创建一个新对象,并将该对象的原型设置为传入的原型对象。可以通过在原型对象上定义属性和方法来设置新对象的属性和方法。

例如:

const person = {
  name: 'John',
  sayHello: function() {
    console.log('Hello!');
  }
};

const obj = Object.create(person);

在这个例子中,obj是通过person对象作为原型对象来创建的。因此,obj继承了person对象的属性和方法。

总的来说,new Object()适用于创建一个简单的对象,而Object.create()适用于创建一个继承自其他对象的新对象。

21、关于this的场景题

好的,下面给你一个关于this的场景题。

假设有一个对象person,它有一个属性name和一个方法sayHellosayHello方法中使用了this关键字。现在我们将sayHello方法赋值给一个变量hello,然后再调用hello方法。请问,此时sayHello方法中的this关键字指向谁?

const person = {
  name: 'John',
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

const hello = person.sayHello;
hello();

在这个例子中,sayHello方法中的this关键字指向调用它的对象。也就是说,当我们通过person对象调用sayHello方法时,this关键字指向person对象,输出结果为Hello, my name is John。但是当我们将sayHello方法赋值给变量hello后再调用hello方法时,this关键字指向全局对象(在浏览器环境中为window对象),因为此时hello方法是在全局作用域中被调用的。因此,输出结果为Hello, my name is undefined,因为全局对象没有name属性。

22、关于作用域和自由变量的场景题 - 1

假设有一个函数 outer,其中定义了一个变量 x 和一个内部函数 inner,代码如下:

function outer() {
  var x = 1;
  function inner() {
    console.log(x);
  }
  return inner;
}

var innerFunc = outer();
innerFunc(); // 输出 1

在这个例子中,outer 函数定义了一个变量 x 和一个内部函数 inner,并且将 inner 函数作为返回值返回。然后,我们将 outer 函数的返回值赋值给变量 innerFunc,并调用 innerFunc 函数。

在调用 innerFunc 函数时,它会输出变量 x 的值,也就是 1。这是因为 inner 函数可以访问到 outer 函数中定义的变量 x,这个变量被称为自由变量。

在 JavaScript 中,自由变量是指在一个函数中使用的变量,但是这个变量并没有在这个函数中被声明。当一个函数被调用时,它会先从自己的作用域中查找变量,如果没有找到,就会继续向外层作用域查找,直到找到为止。

在这个例子中,inner 函数中使用了变量 x,但是这个变量并没有在 inner 函数中被声明,所以它是一个自由变量。当 innerFunc 函数被调用时,它会从外层作用域中查找变量 x,最终找到了 outer 函数中定义的变量 x,所以输出了 1

23、判断字符串以字母开头,后面字母数字下划线,长度6-30

你可以使用正则表达式来实现这个需求,具体实现如下:

function isValid(str) {
  const regExp = /^[a-zA-Z][a-zA-Z0-9_]{5,29}$/;
  return regExp.test(str);
}

这里的正则表达式 /^[a-zA-Z][a-zA-Z0-9_]{5,29}$/ 由以下部分组成:

  • ^ 表示字符串的开头
  • [a-zA-Z] 表示以字母开头
  • [a-zA-Z0-9_] 表示后面的字符可以是字母、数字或下划线
  • {5,29} 表示字符总长度为 6-30
  • $ 表示字符串的结尾

test() 方法用于检测一个字符串是否匹配某个正则表达式,如果匹配则返回 true,否则返回 false。

你可以这样调用这个函数:

console.log(isValid('abc123')); // true
console.log(isValid('1abc_123')); // false
console.log(isValid('a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y_z')); // true
console.log(isValid('a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y_z1')); // false

如果函数返回 true,则说明传入的字符串符合要求,否则不符合。

24、关于作用域的自由变量的场景题 - 2

假设我们有一个全局变量 x,并且在一个函数中定义了一个变量 y,代码如下:

var x = 1;

function foo() {
  var y = 2;
  console.log(x + y);
}

foo(); // 输出 3

在这个例子中,全局变量 x 可以在任何地方被访问,因为它是在全局作用域中定义的。而变量 y 只能在函数 foo 中被访问,因为它是在函数作用域中定义的。

当函数 foo 被调用时,它会从自己的作用域中查找变量 xy,如果没有找到,就会继续向外层作用域查找,直到找到为止。

在这个例子中,函数 foo 中使用了变量 xy,它们都可以在函数作用域中被访问。变量 x 是一个全局变量,所以可以直接访问。而变量 y 是在函数作用域中定义的,所以只能在函数 foo 中被访问。

25、手写字符串trim方法,保证浏览器兼容性

trim() 方法用于去除一个字符串两端的空格符、制表符、换行符等空白字符。以下是一个手写的 trim() 方法的实现,保证在各种浏览器中都能正常使用:

if (!String.prototype.trim) {
  String.prototype.trim = function () {
    return this.replace(/^\s+|\s+$/g, '');
  };
}

这里的代码首先检查 String.prototype 对象中是否已经有了 trim() 方法,如果没有则定义一个。接着,使用正则表达式 /^\s+|\s+$/g 匹配字符串两端的空白字符,并使用空字符串 '' 替换它们。

你可以这样调用这个 trim() 方法:

const str = '  hello, world!  ';
console.log(str.trim()); // 'hello, world!'

注意,这个方法只会去除字符串两端的空白字符,如果字符串中间有空白字符,则不会去除。

26、如何获取多个数字中的最大值

你可以使用Math对象中的max()方法来获取多个数字中的最大值。max()方法可以接受任意数量的参数,并返回参数中的最大值。

例如,如果你有一个数组numbers包含多个数字,你可以使用以下代码来获取最大值:

var numbers = [1, 5, 3, 9, 2];
var maxNumber = Math.max.apply(null, numbers);
console.log(maxNumber); // 输出 9

在这个例子中,我们使用apply()方法将数组numbers中的每个元素作为参数传递给max()方法。因为max()方法是一个Math对象的方法,所以我们需要使用apply()方法来调用它。我们将null作为apply()方法的第一个参数传递,因为我们不需要改变max()方法中的this值。

27、如何用JS实现继承

在JavaScript中,可以通过原型链实现继承。具体实现方法如下:

  1. 定义一个父类,可以是一个构造函数或者一个对象字面量。
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log('My name is ' + this.name);
}
  1. 定义一个子类,通过调用父类的构造函数来继承父类的属性。
function Cat(name, color) {
  Animal.call(this, name);
  this.color = color;
}
  1. 将子类的原型对象指向父类的实例,实现对父类方法的继承。
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

在这个例子中,我们使用Object.create()方法创建了一个新的对象,将其原型对象指向了Animal.prototype。这样,Cat.prototype就可以访问Animal.prototype中的方法了。最后,我们将Cat.prototype.constructor指向Cat,以保证Cat.prototype.constructor的正确性。

现在,我们就可以创建一个Cat对象,并调用它的方法了:

var cat = new Cat('Tom', 'gray');
cat.sayName(); // 输出 "My name is Tom"

这样,我们就通过原型链实现了继承。

28、如何捕获JS程序中的异常

在 JavaScript 中,可以使用 try-catch 语句来捕获异常。try 块中包含可能会抛出异常的代码,如果异常被抛出,catch 块会捕获并处理异常。

以下是一个示例:

try {
  // 可能会抛出异常的代码
  throw new Error('这是一个异常');
} catch (error) {
  // 处理异常
  console.log(error.message);
}

在 try 块中,我们手动抛出了一个异常。catch 块捕获了这个异常,并将异常信息打印到控制台上。

除了 try-catch 语句,还可以使用 try-finally 语句来确保在发生异常时执行清理操作。finally 块中的代码无论是否发生异常都会执行。

以下是一个示例:

try {
  // 可能会抛出异常的代码
  throw new Error('这是一个异常');
} catch (error) {
  // 处理异常
  console.log(error.message);
} finally {
  // 清理操作
  console.log('完成清理操作');
}

在这个示例中,无论是否发生异常,finally 块中的代码都会执行。

29、什么是JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它基于 JavaScript 语言的一个子集,并且易于阅读和编写。它的设计目的是用于数据的传输和存储,并且可以被任何编程语言读取和解析。JSON 格式的数据可以在客户端和服务器之间进行传输,并且可以轻松地转换为 JavaScript 对象。

JSON 数据格式由两种结构组成:键值对和数组。键值对由键和值组成,用冒号分隔,多个键值对之间用逗号分隔,整个结构用花括号括起来。数组由多个值组成,用逗号分隔,整个结构用方括号括起来。

以下是一个简单的 JSON 数据示例:

{
  "name": "John",
  "age": 30,
  "city": "New York",
  "hobbies": ["reading", "music", "sports"]
}

在这个示例中,我们有四个键值对,分别是 name、age、city 和 hobbies。其中 hobbies 的值是一个数组,包含三个字符串值。这个 JSON 数据可以轻松地转换为 JavaScript 对象,如下所示:

{
  name: "John",
  age: 30,
  city: "New York",
  hobbies: ["reading", "music", "sports"]
}

JSON 数据格式已经成为现代 Web 应用程序中常用的数据交换格式,因为它易于阅读和编写,并且可以被任何编程语言解析。

30、获取当前页面的url参数

获取当前页面的URL参数可以使用JavaScript中的location.search属性。location.search返回当前URL中的查询字符串,包括问号(?)和后面的参数。您可以使用这个属性来获取当前URL中的所有参数,然后将其解析为JavaScript对象。以下是一个示例代码:

function getParams() {
  var params = {};
  var queryString = location.search.substring(1);
  var vars = queryString.split("&");
  for (var i=0; i<vars.length; i++) {
    var pair = vars[i].split("=");
    params[pair[0]] = decodeURIComponent(pair[1]);
  }
  return params;
}

// 使用示例
var params = getParams();
console.log(params);

在这个示例中,我们定义了一个名为getParams的函数,该函数从当前URL中获取所有参数,并将它们解析为JavaScript对象。该函数首先通过location.search属性获取查询字符串,然后使用split方法将其拆分为一个包含所有参数的数组。接下来,我们使用for循环迭代该数组,并使用split方法将每个参数拆分为名称和值对。最后,我们将这些名称和值对存储在一个名为params的对象中,并使用decodeURIComponent方法解码值,以便正确处理特殊字符。最终,我们返回这个params对象,它包含了当前URL中的所有参数。

31、手写深拷贝

深拷贝是指在拷贝一个对象时,不仅会拷贝对象本身,还会递归地拷贝对象中所有的子对象。以下是一个手写的深拷贝函数:

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  var clone = {};
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key]);
    }
  }
  return clone;
}

在这个函数中,我们首先检查传递的对象是否为null或非对象类型。如果是,我们直接返回该对象。否则,我们创建一个空的对象clone,并使用for-in循环遍历原始对象的所有属性。对于每个属性,我们使用递归调用深拷贝函数来拷贝它的值,并将其存储在clone对象中。最后,我们返回clone对象,它是原始对象的一个深拷贝副本。

需要注意的是,这个函数并不能处理循环引用的情况,即如果对象中存在循环引用,该函数会进入死循环并最终导致堆栈溢出。为了避免这种情况,我们需要在函数中添加一个额外的参数,用于跟踪已经遍历过的对象,并在遍历对象时检查是否已经遍历过该对象。

32、介绍一下RAF requestAnimateFrame

RAF,全称为requestAnimationFrame,是一种浏览器提供的API,用于在下一次浏览器重绘之前执行指定的动画操作。相比于使用setTimeout或setInterval实现动画,RAF具有更好的性能和更流畅的动画效果。

使用RAF的基本流程是:在每一帧动画开始前,调用requestAnimationFrame方法请求浏览器在下一帧动画之前调用指定的回调函数。在回调函数中进行动画操作,然后再次调用requestAnimationFrame方法请求下一帧动画。这样就可以实现高性能的动画效果。

示例代码如下:

function animate() {
  // 在每一帧动画开始前,调用requestAnimationFrame方法请求下一帧动画
  requestAnimationFrame(animate);
  
  // 在回调函数中进行动画操作
  // ...
}

// 启动动画
animate();

需要注意的是,使用RAF时需要确保动画操作不会耗费过多的资源,否则可能导致性能问题。同时,由于RAF是浏览器提供的API,不同浏览器的实现可能存在差异,因此需要进行兼容性处理。

33、前端性能如何优化? 一般从哪几个方面考虑

前端性能优化是一个很广泛的话题,一般可以从以下几个方面考虑:

  1. 减少HTTP请求:减少页面中的资源请求可以大大提高页面加载速度。可以通过合并CSS和JavaScript文件、使用CSS Sprites、压缩图片等方式来减少HTTP请求。

  2. 压缩和合并文件:可以使用工具将多个CSS或JavaScript文件合并成一个文件,并压缩文件大小,以减少文件下载时间和页面加载时间。

  3. 使用缓存:使用缓存可以避免重复请求相同的资源,提高页面的加载速度。可以使用浏览器缓存、CDN缓存等方式来实现缓存。

  4. 优化图片:图片是网页中最常见的资源类型,优化图片可以大大提高页面加载速度。可以使用合适的图片格式、压缩图片大小、使用懒加载等方式来优化图片。

  5. 减少DOM操作:DOM操作是非常耗费性能的操作,应该尽量减少DOM操作次数,尽量使用批量操作或者缓存DOM元素等方式来减少DOM操作。

  6. 使用异步加载:可以使用异步加载来提高页面加载速度。可以使用异步加载JS、使用defer和async属性等方式来实现异步加载。

  7. 优化JavaScript代码:JavaScript代码是页面中最常见的脚本类型,优化JavaScript代码可以提高页面的响应速度和性能。可以使用代码压缩、避免全局变量、使用事件委托等方式来优化JavaScript代码。

综上所述,前端性能优化需要从多个方面考虑,需要根据具体情况选择合适的优化方式。