JavaScript 知识总结上篇(更新版)

1. 为什么 JS 是单线程的?

因为JS里面有可视的Dom,如果是多线程,这个线程正在删除DOM节点,另一个线程正在编辑Dom节点,导致浏览器不知道该听谁的

2.如何理解同步和异步?

同步:按照代码书写顺序执行处理指令的一种模式,上一段代码执行完才能执行下一段代码。

异步:并行处理的方式,不必等待一个程序执行完,可以执行其它的任务。

JS之所以需要异步的原因:JS是单线程运行的

常用的异步场景有:定时器、ajax请求、事件绑定。

3. JS 是如何实现异步编程的?

1) 回调函数callback:将需要异步执行的函数作为回调函数执行

function fetchData(callback) {
  // 模拟 从服务器获取数据 的异步操作
  setTimeout(() => {
    const data = {
        type: 1
    }
    callback(null, data) // 将获取的数据传递给回调函数
  }, 1000)
}

// 定义回调函数
function processFetchedData(err, data) {
  if (err) {
    console.error('发生错误:', err)
  } else {
    console.log('处理获取到的数据:', data)
  }
}

// 调用异步函数,并传入回调函数
fetchData(processFetchedData)

优点:解决了同步的问题

缺点:回调地狱(回调嵌套层数太多,代码结构混乱),不能用 try catch 捕获错误,不能 return

2) Promise对象:通过链式调用的方式

function asyncOperation() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      // 异步操作完成后,调用resolve表示成功,调用reject表示失败
      resolve('异步操作完成')
    }, 1000)
  })
}

asyncOperation()
  .then(function(result) {
    console.log(result)
  })
  .catch(function(error) {
    console.log(error)
  })

优点:解决了回调地狱的问题

缺点:无法取消 Promise,错误需要通过回调函数来捕获

3) async/await:async关键字用于声明一个异步函数,await关键字用于等待一个Promise对象的解析结果

async function asyncOperation() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      // 异步操作完成后,调用resolve表示成功,调用reject表示失败
      resolve('异步操作完成')
    }, 1000)
  })
}

(async function() {
  try {
    const result = await asyncOperation()
    console.log(result)
  } catch (error) {
    console.log(error)
  }
})()

优点:基于Promise实现的异步函数,代码清晰,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能降低

注:使用 async 关键声明函数会隐式返回一个Promise

4) 事件监听模式:采用事件驱动的思想,当某一事件发生时触发执行异步函数

缺点:整个代码全部变为事件驱动模式,难以分辨主流程

5) 发布订阅模式:当异步任务执行完成时发布消息给信号中心,其他任务通过在信号中心 中订阅消息来确定自己是否开始执行

const eventEmitter = {
  // events属性用于保存事件
  events: {},
  
  // events对应的回调函数---订阅事件
  // subscribe 将回调函数添加到对应的事件列表中
  subscribe(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = []
    }
    
    this.events[eventName].push(callback)
  },
  
  // events对应的回调函数---发布事件
  // publish 遍历对应事件列表中的回调函数,通过setTimeout设置异步调用,模拟异步操作
  publish(eventName, data) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(function(callback) {
        setTimeout(function() {
          callback(data)
        }, 0)
      })
    }
  }
}

// 订阅事件
eventEmitter.subscribe('event1', function(data) {
  console.log('event1:', data)
})

eventEmitter.subscribe('event2', function(data) {
  console.log('event2:', data)
})

// 异步操作完成后发布事件
setTimeout(function() {
  eventEmitter.publish('event1', '异步操作完成')
}, 1000)

setTimeout(function() {
  eventEmitter.publish('event2', '异步操作完成')
}, 2000)

 这段代码的执行顺序如下:

1. 订阅事件:首先,通过`eventEmitter.subscribe`方法订阅了两个事件,分别是`event1`和`event2`。传入的回调函数会在对应的事件被发布时执行。

2. 异步操作:之后,通过两个`setTimeout`函数模拟了两个异步操作,分别在1秒后和2秒后完成。

3. 发布事件:在相应的时间到达后,通过`eventEmitter.publish`方法发布了两个事件,即`event1`和`event2`。传递的数据是`'异步操作完成'`。

4. 执行回调函数:异步操作完成后,对应的事件回调函数会被执行。根据代码中的设置,`event1`的回调函数会立即执行,并打印`'event1: 异步操作完成'`。而`event2`的回调函数会在1秒后执行,并打印`'event2: 异步操作完成'`。

因此,代码的执行顺序是:

- 0秒:订阅事件
- 1秒:发布`event1`事件并执行回调函数,输出`event1: 异步操作完成`
- 2秒:发布`event2`事件并执行回调函数,输出`event2: 异步操作完成`

可以看到,订阅事件的操作是立即执行的,而发布事件和执行回调函数是异步的,根据设定的延迟时间来执行的。

6) 利用生成器实现

4. 宏任务和微任务都有哪些?

宏任务:整体代码script、setTimeOut、setInterval、setImmediate、I/O、页面渲染UI rendering等

执行顺序如下:

  • 当执行栈为空时,会从宏任务队列中取出一个任务执行。
  • 执行完当前宏任务后,会检查微任务队列。
  • 如果微任务队列不为空,则依次执行微任务直到微任务队列为空。
  • 然后,更新界面渲染。
  • 最后,再次从宏任务队列中取出任务执行。

微任务:Promise、process.nextTick、Object.observe、MutationObserver等

执行顺序如下:

  • 在当前宏任务执行完成后,当存在微任务时,会将所有微任务放入一个微任务队列中。
  • 等待宏任务执行完毕后,从微任务队列中取出所有的微任务按顺序执行。
  • 在执行微任务的过程中,如果又产生新的微任务,会继续添加到微任务队列的末尾。
  • 直到微任务队列为空。

注意:Promise是同步任务

5. 宏任务和微任务的执行

执行宏任务

执行所有微任务

更新界面渲染

重复上述步骤,直至宏任务队列为空

6. 为什么要使用模块化?

模块化是一种管理代码的方法,可以将代码分割为不同的模块或文件,并通过特定的方式来管理它们之间的依赖关系和导出关系,使得代码的结构更加清晰、易于维护和扩展。

JavaScript没有原生支持模块化时,开发者常使用命名空间和立即执行函数等方式来实现模块化。会出现:命名空间容易造成命名冲突,立即执行函数存在代码冗余和性能问题等。

7.都有哪几种方式可以实现模块化,各有什么特点?

命名空间模式:通过为全局对象添加属性来避免命名冲突,例如将模块的所有函数和变量放在一个对象中,以避免与全局命名空间冲突。但这种方式会导致命名空间变得臃肿,难以维护。

IIFE 模式:使用立即执行函数表达式来创建私有作用域,从而避免命名冲突。这种方式也称为模块模式,它使用一个匿名函数来封装模块,并返回一个公共接口。

CommonJS 模块化:使用 require() 导入模块,exports 导出模块,使得模块可以在不同的环境中使用。Node.js 采用了 CommonJS 标准来实现模块化。

AMD 模块化:支持异步加载模块,使用 define() 来定义模块,使用 require() 来加载模块。

ES6 模块化:ES6 引入了原生的模块化系统,可以使用 import 和 export 语句导入和导出模块。它提供了静态分析功能,可以在编译时进行优化,使得模块加载更快。

8. JS 模块包装格式有哪些?

Commonjs:同步运行,不适合前端

AMD:异步运行,RequireJS规范

CMD:异步运行,seajs 规范

9. ES6 和 Commonjs 的区别?

Commonjs模块输出的是值的拷贝,ES6输出的值是值的引用

Commonjs是在运行时加载,是一个对象,ES6是在编译时加载,是一个代码块

Commonjs的this指向当前模块,ES6的this指向undefined

10.require/import 之间的区别?

(1)require是CommonJS语法,import是ES6语法;

(2)require只在后端服务器支持,import在高版本浏览器及Node中都可以支持;

(3)require引入的是原始导出值的复制,import则是导出值的引用;

(4)require是运行时动态加载,import是静态编译;

(5)require默认调用不是严格模式,import默认调用严格模式

11. exports 和 module.exports 有什么区别?

导出方式不一样

exports.xxx='xxx'

module.exports = {}

exports是module.exports的引用,两个指向的是同一个地址,但require只能看到module.exports

12.闭包

指能够读取其他函数内部变量的函数,或子函数在外调用时, 子函数所在父函数的作用域不会被释放

function outerFunction() {
  var outerVariable = 'Hello'; // 外部函数的变量

  function innerFunction() { // 内部函数
    console.log(outerVariable); // 内部函数访问外部函数的变量
  }

  return innerFunction; // 返回内部函数
}

var myFunction = outerFunction(); // 返回内部函数,并赋值给变量
myFunction(); // 执行内部函数,打印 'Hello'

13.闭包的用途

1).读取函数内部的变量

2).让这些变量的值始终保持在内存中。不会在f1调用后被自动清除。

3).方便调用上下文的局部变量,利于代码封装。

原因:f1是f2的父函数,f2被赋给了一个全局变量,f2始终在内存中,f2的存在依赖f1,因此f1始终存在内存中,不会在调用结束后,被垃圾回收机制回收。

14.闭包的缺点

1).由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2).闭包会在父函数外部,改变父函数内部变量的值。如果把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时不要随便改变父函数内部变量的值。

15.JS 有哪些数据类型?

基本数据类型:Undefined、Null、Boolean、Number、String、Symbol、BigInt

Symbol 和 BigInt 是 ES6新增的数据类型

  • Symbol 表示创建后独一无二且不可变的数据类型,主要是为了解决可能出现全局变量冲突的问题
  • BigInt  表示任意精度格式的整数,可以安全的存储和操作大整数,即使这个整数超出了Number 能够表示的安全整数范围

引用数据类型:只有Object一种,主要包括对象、数组和函数

16.基本数据类型和引用数据类型有什么区别?

两者作为函数的参数进行传递时:

  • 基本数据类型传入的是数据的副本,原数据的更改不会影响传入后的数据。
  • 引用数据类型传入的是数据的引用地址,原数据的更改会影响传入后的数据。

两者在内存中的存储位置:

  • 基本数据类型存储在栈中
  • 引用数据类型在栈中存储了指针,该指针指向的数据实体,存储在堆中

17.判断数据类型的方法有哪些?

(1)利用 typeof 判断数据类型,其中数组,对象和 null 都会被判断成 object;

console.log(typeof([1, 2, 3])); //object
console.log(typeof(1)); //number
console.log(typeof('abc')); //string
console.log(typeof(false)); //boolean
console.log(typeof(null)); //object
console.log(typeof(new Date())); //object
console.log(typeof(undefined)); //undefined
console.log(typeof(function () {})); //function
console.log(typeof(temp)); //undefined

(2)A instanceof B 可以用来判断A是否为B的实例,其内部机制是判断在其原型链中能否找到该类型的原型,但它不能检测 null 和 undefined;

// 只能判断引用数据类型
console.log([1, 2, 3] instanceof Array); // true
console.log(function() {} instanceof Function); // true
console.log({} instanceof Object); // true

// 基本数据类型不能判断
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('abc' instanceof String); // false

(3)B.constructor === A 可以判断A是否为B的原型,对象实例可以通过 constructor 对象访问它的构造函数【如果创建一个对象来改变它的原型,constructor 就不能用来判断数据类型】;

// 判断数据类型
console.log((2).constructor === Number); // true
console.log((true).constructor === Bollean); // true
console.log(('abc').constructor === String); // true
console.log(([1, 2]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

// 对象实例
function Fn() {};
Fn.prototype = new Array();

var f = new Fn();

console.log(f.constructor === Fn); // false
console.log(f.constructor === Array); // true

(4)Object.prototype.toString.call() 使用 Object 对象的原型方法 toString() 来判断数据类型。

let a = Object.prototype.toString;
console.log(a.call(1)); // [object Number]
console.log(a.call(true)); // [object Boolean]
console.log(a.call('abc')); // [object String]
console.log(a.call([])); // [object Array]
console.log(a.call(function() {})); // [object Function]
console.log(a.call()); // [object Undefined]
console.log(a.call(undefined)); // [object Undefined]
console.log(a.call(null)); // [object Null]

18.检测对象 obj 调用的两种 toString 方法什么区别

obj.toString() 和 Object.prototype.toString.call(obj) 的结果不一样:

每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。

默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中 type 是对象的类型。

console.log([1, 2, 3].toString()); // '1, 2, 3'

数组 [1, 2, 3] 调用 toString() 返回字符串 '1, 2, 3' ,因为 Array 和 Object 里都有 toString() 

// 检测 Array 里的 toString
console.log(Array.prototype.hasOwnProperty('toString')); // true

根据原型链的就近原则,会先取 Array.prototype,如果删除 Array 里的 toString()

// 删除 Array 里的 toString
console.log(delete Array.prototype.toString); // true

此时 Array 的 toString,是继承 Object 里的 toString。 如果此时调用 toString() 就可以判断数据类型,不会输出之前的字符串了

console.log([1, 2, 3].toString()); // '[object Array]'

所以直接用对象上面的 Object.prototype.toString 来判断数据类型,此时会输出

console.log(Object.prototype.toString([])); // '[object Object]'

此时之所以没有输出我需要判断的 “数组” 的数据类型 Array,是因为 Object.prototype 本身是对象类型,所以返回对象类型,想判断传入值的类型,需要把传入值的this 指向 Object.prototype

console.log(Object.prototype.toString.call([])); // '[object Array]'

19.原型和原型链是什么?

什么是构造函数

用于在创建对象时 初始化对象

特点:

  • 构造函数名一般为大写字母开头
  • 与 new 运算符一起使用来实例化对象
function Fn() {}; // 构造函数 Fn
var f = new Fn(); // 构造函数 Fn 创建对象,也叫实例化

原型(箭头函数没有原型)

每个函数都有 prototype 属性,称之为原型【这个属性的值是个对象,所以也称为原型对象】。

原型的作用:

1.存放一些属性和方法,共享给实例对象用

2.在js中实现继承

首先通过 构造函数 创建一个数组 arr:

const arr = new Array(1, 2, 3);

构造函数 Array 是一个函数,有 Array.prototype 原型 属性,在原型身上挂载了许多方法,因此 构造函数 Array 生成的实例 arr 就可以使用 Array.prototype 原型身上的方法:

arr.reverse();
arr.toString();

为什么实例可以使用原型身上的方法?

JavaScript 的每个实例对象中都包含一个 _proto_ 内部属性,这个属性指向它的原型对象

console.log(arr._proto_ === Array.prototype); // true

 

原型链

每个实例对象中都包含一个 _proto_ 内部属性,这个属性指向它的原型对象,原型对象也是对象,也具有 _proto_ 属性,指向原型对象的原型对象,当一个对象调用自身不存在的属性/方法时,依次类推,直到找到属性/方法或 返回 null 为止,从而形成链式结构称为 “原型链”。

20.什么是执行上下文和执行栈?

执行上下文(Execution Context)是在代码执行期间用于管理变量、函数和作用域的内部数据结构。每当JavaScript代码执行时,都会创建一个执行上下文。

执行栈(Execution Stack),也称为调用栈(Call Stack),是用于管理执行上下文的一种数据结构。它是一个栈结构(后进先出LIFO),用于追踪代码的执行位置和控制程序的流程。

当JavaScript代码开始执行时,首先会创建一个全局执行上下文(Global Execution Context),代表全局作用域。然后,在执行过程中,每当函数被调用时,都会创建一个新的执行上下文,并被推入执行栈的顶部。当函数执行完成后,对应的执行上下文会从执行栈中弹出,控制权会回到上一个执行上下文。

执行栈的主要作用是:

  • 跟踪函数的调用顺序和执行位置
  • 管理执行上下文,包括变量、函数和作用域的创建、访问和释放
  • 控制程序的流程,包括函数的调用和返回
function add(a, b) {
  var sum = a + b;
  return sum;
}

function multiply(a, b) {
  var product = a * b;
  var result = add(product, product);
  return result;
}

var x = 2;
var y = 3;
var z = multiply(x, y);
console.log(z);

在上述示例中,首先创建了一个全局执行上下文,然后依次调用了multiply函数、add函数,最后打印了结果z

执行过程如下:

  1. 全局执行上下文被创建,全局变量xy被初始化。
  2. 调用multiply函数,创建multiply函数的执行上下文,并被推入执行栈的顶部。
  3. multiply函数的执行上下文中,变量ab被初始化。
  4. 调用add函数,创建add函数的执行上下文,并被推入执行栈的顶部。
  5. add函数的执行上下文中,变量ab被初始化。
  6. add函数执行完成,add函数的执行上下文从执行栈中弹出。
  7. multiply函数执行完成,multiply函数的执行上下文从执行栈中弹出。
  8. 返回到全局执行上下文,打印结果z
  9. 全局执行上下文被销毁。

21.什么是作用域和作用域链?

作用域是标识符所能生效的范围。作用域最大的用处是隔离变量,不同作用域下同名变量不会有冲突。ES6中有全局作用域、函数作用域和块级作用域

当一个变量在当前块级作用域中未被定义时,会向父级作用域寻找。如果父级仍未找到,会再向上寻找,直到找到全局作用域为止。这种一层一层的关系,就是作用域链:

内部作用域->外部作用域-> 全局作用域

作用域链的形成是在定义函数的时候确定的,它基于函数定义时所在的作用域和函数内部的变量。当函数被调用时,会创建一个新的执行环境,并且该执行环境会包含该函数的作用域链,从而确保函数内部可以访问到所需的变量。

22.作用域和执行上下文的区别是什么?

1)函数的执行上下文只在函数被调用时生成,作用域在创建时已经生成

2)函数的作用域会包含若干个执行上下文(当函数未被调用时有可能是零个执行上下文)。

23.如何改变 this 指针的指向?

使用apply、call、bind方法改变this指向(并不会改变函数的作用域)。比较如下:

apply方法:

通过apply方法可以调用一个函数,并且将指定的对象作为函数的上下文(this)对象。它接受两个参数,第一个参数是要绑定给函数的this对象,第二个参数是一个数组或类数组对象,表示传递给函数的参数。语法如下:

function Fn(arg1, arg2, ...) {
  // 函数体
}

Fn.apply(thisArg, [arg1, arg2, ...]);

call方法:

通过call方法可以调用一个函数,并且将指定的对象作为函数的上下文(this)对象。它接受一个参数列表,第一个参数是要绑定给函数的this对象,后面的参数是传递给函数的参数。语法如下:

function Fn2(arg1, arg2, ...) {
  // 函数体
}

Fn2.call(thisArg, arg1, arg2, ...);

bind方法:

通过bind方法可以创建一个新的函数,并且将指定的对象作为函数的上下文(this)对象。它会返回一个新的函数,原函数不会被执行。bind方法接受一个参数列表,第一个参数是要绑定给函数的this对象,后面的参数是传递给函数的参数。语法如下:

function Fn3(arg1, arg2, ...) {
  // 函数体
}

var newFunction = Fn3.bind(thisArg, arg1, arg2, ...);

这三个方法的区别在于参数的传递方式和函数的执行时机。apply和call是立即执行函数,并且可以传递参数列表,而bind方法会返回一个新的函数,需要手动调用才会执行。

24. 什么是 AJAX?如何实现?Ajax和Axios什么区别?

Ajax是用于实现局部网页异步刷新的技术,Ajax的实现主要包括四个步骤:

(1)创建核心对象XMLhttpRequest;

(2)利用open方法打开与服务器的连接;

(3)利用send方法发送请求("POST"请求时,还需额外设置请求头)

(4)监听服务器响应,接收返回值。

Ajax 和 Axios 都用于在浏览器中发送异步请求的技术,区别如下:

1. 语法和 API:Ajax 使用原生的 XMLHttpRequest 对象来发送和处理异步请求,需要手动设置请求头、请求方法等。而 Axios 提供了更简洁、易于使用的 API,且支持 Promise。

2. 浏览器兼容性:Ajax 是一种原生的 JavaScript 技术,所以可以在大多数现代浏览器中使用。而 Axios 是一个第三方库,可以在支持 Promise 的浏览器中使用。

3. 功能和扩展性:Ajax 提供了基本的发送异步请求和处理响应的功能,如果需要更复杂的功能,可能需要自己实现或使用其他库。Axios 提供了更丰富的功能,如拦截请求和响应、请求取消、自动转换响应数据等,以及更好的错误处理机制。

25.浅拷贝与深拷贝有何区别?如何实现?

浅拷贝:是指创建一个新对象或数组,并将原始对象或数组的引用复制到新对象或数组中。浅拷贝只复制了对象或数组的第一层属性或元素,而不复制嵌套的对象或数组。如果修改了原始对象或数组的嵌套对象或数组,浅拷贝的新对象或数组也会受到影响。

浅拷贝的实现方式:

(1)Object.assign():多维时对内层对象是浅拷贝;目标对象只有一层的时候,是深拷贝

(2)扩展运算符:使用...将一个数组或对象展开,实现浅拷贝

(3)Array.slice():返回一个新数组,包含原始数组中的一部分元素。适用于数组的浅拷贝

深拷贝:是指完全复制一个对象或数组,包括嵌套的对象或数组。深拷贝会递归复制所有层级的属性或元素,创建一个全新的对象或数组,与原始对象或数组完全独立,互不影响。

深拷贝的实现方式:

(1)JSON.parse(JSON.stringify()):将对象转换为字符串,再将字符串转换为新的对象,实现深拷贝。但该方法无法处理包含函数、正则表达式等特殊对象的情况。

(2)递归复制:自定义递归函数来复制对象或数组的每一层,并创建新的对象或数组。适用于更复杂的深拷贝场景

手写一个深拷贝:

function deepCopy(obj) {
  // 检查是否为对象或数组
  if (typeof obj !== "object" || obj === null) {
    return obj; // 基本类型直接返回
  }
  // 根据原对象的类型创建目标对象
  const target = Array.isArray(obj) ? [] : {};
  // 遍历原对象的属性或元素
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 递归复制每个属性或元素
      target[key] = deepCopy(obj[key]);
    }
  }
  return target;
}

// 使用上述的深拷贝函数可以复制对象或数组的每一层,创建独立的目标对象。
const obj = {
  name: "John",
  age: 30,
  address: {
    city: "New York",
    country: "USA"
  }
};

const copiedObj = deepCopy(obj);
copiedObj.name = "Jane";
copiedObj.address.city = "San Francisco";

console.log(obj); 
// { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }
console.log(copiedObj); 
// { name: 'Jane', age: 30, address: { city: 'San Francisco', country: 'USA' } }

26.事件委托

事件委托(事件代理):利用事件冒泡,把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,委托就无法实现。

27.变量的提升

提升用来描述变量和函数移动到其(全局或函数)作用域顶部。

console.log(name); // undefined
var name = "John";

// 等价于

var name; // 变量声明被提升到作用域顶部
console.log(name); // undefined
name = "John";

变量 name 的声明被提升到了作用域顶部,所以 console.log 输出变量name的值之前,即使变量 name 还没被赋值,但并不会报错,而是输出 undefined。 

console.log(name); // ReferenceError: name is not defined
let name = "John";

 使用let和const声明的变量以及函数表达式不会被提升。

执行上下文有两个阶段:编译 和 执行 。

编译-在此阶段,JS 获取所有函数声明,并将其提升到其作用域的顶部,以便稍后可以引用它们,并获取所有使用 var 关键字声明的变量,默认值:undefined

执行-在此阶段,它将值赋给之前提升的变量,并执行或调用函数(对象中的方法)。

注意:函数表达式或箭头函数,let 和 const 声明的变量,都不会被提升

28.高阶函数

将函数作为参数或返回值的函数。

29. undefined  null 有什么区别

它们都属于虚值,可使用 Boolean(value) 或 !!value 将其转换为布尔值时,值为 false 。

undefined 是未指定特定值的变量的默认值,或者没有显式返回值的函数

let _thisIsUndefined;

console.log(_thisIsUndefined);  // undefined

null 是“不代表任何值的值”。 null 是已明确定义给变量的值。

fs.readFile((e) => {

        console.log(e); // 当没有错误发生时,打印 null

});

在比较 null 和 undefined 时,我们使用 == 时得到 true ,使用 === 时得到 false

30.== 和 === 以及 Object.js 的区别

"==="严格运算符:如果两个值的类型不同,返回false;相同类型返回true;

两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个对象

"=="相等运算符:在比较不同类型的数据时,相等运算符会先将数据进行类型转换,再用严格相等运算符比较

Object.js 主要的区别:+0 != -0,但 NaN == NaN

假设比较 x == y:

1. 如果 x 和 y 的类型相同,会换成 === 操作符进行比较。

2. 如果 x 为 null , y 为 undefined ,则返回 true 。

3. 如果 x 为 undefined 且 y 为 null ,则返回 true 。

4. 如果 x 的类型是 number , y 的类型是 string ,那么返回 x == toNumber(y) 。

5. 如果 x 的类型是 string , y 的类型是 number ,那么返回 toNumber(x) == y 。

6. 如果 x 为类型是 boolean ,则返回 toNumber(x)== y 。

7. 如果 y 为类型是 boolean ,则返回 x == toNumber(y) 。

8. 如果 x 是 string 、 symbol 或 number ,而 y 是 object 类型,则返回 x == toPrimitive(y) 。

9. 如果 x 是 object , y 是 string , symbol 则返回 toPrimitive(x) == y 。

10. 剩下的 返回 false

注:toPrimitive 首先在对象中使用 valueOf 方法,然后使用 toString 方法来获取该对象的原始值

如果使用 === 运算符,则第一个示例以外的所有比较将返回 false ,因为它们的类型不同,第一个示例返回 true 是因为两者的类型和值相同。

31. &&和||

&& 逻辑与,在其操作数中找到第一个虚值表达式并返回它,如果没有找到任何虚值表达式,则返回最后一个真值表达式。它采用短路来防止不必要的工作。

|| 逻辑或,在其操作数中找到第一个真值表达式并返回它。这也使用了短路来防止不必要的工作。

32.使用 + 或一元加运算符是将字符串转换为数字的最快方法吗

+ 是将字符串转换为数字的最快方法,因为如果值已经是数字,它不会执行任何操作。

33. 什么是事件传播?

事件流:从页面中接收事件的顺序(即事件传播)

当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。

在“冒泡阶段”中,事件向上传播至父级,祖父母,祖父母或父级,到达window为止;

在“捕获阶段”中,事件从 window 开始向下触发元素事件或 event.target 。

事件传播有三个阶段:

1.捕获阶段window, document, html, grandparent, parent

2.目标阶段:事件已达到目标元素

3.冒泡阶段child, parent, grandparent, html, document, window

34.event.preventDefault()  event.stopPropagation()方法之间有什么区别?

event.preventDefault() 方法可防止元素的默认行为。

在表单元素中使用,它将阻止其提交

在锚元素中使用,它将阻止其导航

在上下文菜单中使用,它将阻止其显示或隐藏

event.stopPropagation() 方法用于阻止捕获和冒泡阶段中当前事件的进一步传播。

35.如何知道是否在元素中使用了event.preventDefault() 方法?

返回一个布尔值false表明在特定元素中调用了event.preventDefault()

<form onsubmit="return validateForm(event)">
  <input type="text" name="name" required>
  <button type="submit">Submit</button>
</form>

<script>
function validateForm(event) {
  // 执行自定义的表单验证逻辑
  if (/* 验证不通过 */) {
    event.preventDefault(); // 阻止表单的默认提交行为
    return false; // 返回false
  }
  return true; // 返回true,允许表单的提交行为
}
</script>

36.为什么此代码 obj.someprop.x 会引发错误?

const obj = {};

console.log(obj.someprop.x);

由于尝试访问 someprop 属性中的 x 属性,而 someprop 并没有在对象中,所以值为 undefined 。记住对象本身不存在的属性,并且其原型的默认值为 undefined 。因为 undefined 没有属性 x ,所以试图访问将会报错。

37.为什么在 JS 中比较两个相似的对象时返回 false

JS 以不同的方式比较对象和基本类型。

在基本类型中,JS 通过值对它们进行比较。

在对象中,JS 通过引用或存储变量的内存中的地址对它们进行比较。

因此,第一个console.log 语句返回 false ,第二个 console.log 语句返回 true

a 和 c 有相同的引用地址,而 a 和 b 没有。

38. !! 运算符能做什么?

!! 运算符可以将右侧的值强制转换为布尔值。

39. 什么是 event.target 

event.target 是发生事件的元素或触发事件的元素

JS 代码如下:

如果单击 button,即使将事件附加在最外面的 div 上,也打印 button 标签,因此event.target 是触发事件的元素。

40.什么是 event.currentTarget

event.currentTarget 是在其上显式附加事件处理程序的元素。

JS 代码如下:

如果单击 button,它会打印最外面的 div 标签。因此,event.currentTarget 是附加事件处理程序的元素。

41. 如何在一行中计算多个表达式的值?

可以使用 逗号 运算符在一行中计算多个表达式。它从左到右求值,并返回右边最后一个项目或最后一个操作数的值。

如图的最后x的结果为27

42. JavaScript 中的虚值是什么?如何检查?

const falseValues = ['', 0, null, undefined, NaN, false];

虚值就是在转换为布尔值时变为 false 的值。使用 Boolean 函数或者 !! 运算符检查

43.use strict

使代码在函数或整个脚本中处于严格模式严格模式帮助我们在代码的早期避免 bug,并为其添加限制

44. 什么是 IIFE,它的用途是什么?

IIFE或立即调用的函数表达式是在创建或声明后将被调用或执行的函数。创建IIFE语法是,将 function (){} 包裹在括号 () 内,然后再用另一个括号 () 调用它即(function(){})()

IIFE的一个主要作用是避免与全局作用域内的其他变量命名冲突或污染全局命名空间

IIFE 还可以用来解决一个常见的面试题:

假设我们有一个带有 list-group 类的 ul 元素,它有 5 个 li 子元素。当单击单个 li 元素时,打印对应的下标值。但这里每次点击 li 打印 i 的值都是 5 ,这是由于闭包。闭包只是函数记住其当前作用域,父函数作用域和全局作用域的变量引用的能力。当在全局作用域内使用 var 关键字声明变量时,就创建全局变量 i 。因此,单击 li 元素时,它将打印 5 ,因为这是稍后在回调函数中引用它时 i 的值。

使用 IIFE 可以解决此问题:

该解决方案因为IIFE会为每次迭代创建一个新的作用域,我们捕获 i 的值并将其传递给currentIndex 参数,因此调用IIFE时,每次迭代的 currentIndex 值都是不同的。

45.arguments 的对象是什么?

arguments 对象是函数中传递的参数值的集合,类似数组,因为它有length属性,可以使用数组索引表示法 arguments[1] 来访问单个值,但它没有数组中的内置方法,如: forEach、reduce、filter、map。可以使用 Array.prototype.slice 将其转换成一个数组。

注意:箭头函数中没有arguments对象。

const four = () => arguments;

four();  // Throws an error - arguments is not defined

当调用函数 four 时,它会抛出一个 ReferenceError: arguments is not defined error

使用 rest 语法,可以解决这个问题。

const four = (...args) => args;

这会自动将所有参数值放入数组中。

46.如何创建一个没有 prototype(原型)的对象?

使用 Object.create 方法创建没有原型的对象。

47.为什么在调用这个函数时,代码中的会变成一个全局变量?

原因是赋值运算符是从右到左的求值的。这意味着当多个赋值运算符出现在一个表达式中时,它们是从右向左求值的。所以上面代码变成了:

表达式 b = 0 求值,b 没有声明。因此,JS引擎在这个函数外创建了一个全局变量 b ,之后表达式 b = 0 的返回值为 0,并赋给新的局部变量 a 。

可以通过在赋值之前先声明变量来解决:

48. var let 和const 的区别

let和var的比较是比较“声明”的问题,let 和 const 是值的修改问题

var声明的变量会挂载在window上,而letconst声明的变量不会

var声明变量存在变量提升,letconst不存在变量提升

letconst声明形成块作用域

if(1){
        var a = 100;
        let b = 10;
        const c = 1;
}
console.log(a); // 100
console.log(b) // 报错:b is not defined ===> 找不到b这个变量
console.log(c) // 报错:c is not defined ===> 找不到c这个变量

同一作用域下letconst不能声明同名变量,而var可以

var a = 100;

console.log(a); // 100

var a = 10;

console.log(a); // 10


let a = 100;

const a = 10;

// 控制台报错:Identifier 'a' has already been declared ==> 标识符a已经被声明

暂存死区--let

const

1.一旦声明必须赋值,不能使用null占位

2.声明后不能再修改

3.如果声明的是复合类型数据,可以修改其属性

49.什么是箭头函数

是一种匿名函数写法

1) 箭头函数表达式没有自己的this, arguments, super, new.target,不能用作构造函数。

它捕获词法作用域函数的 this 值,在此示例中,addAll 函数将复制computeResult 方法中的 this 值,如果在全局作用域声明箭头函数,this 值为 window 对象

2) 可以在箭头函数中使用与函数表达式和函数声明相同的参数。如果在一个箭头函数中有一个参数可以省略括号。

3)箭头函数不能访问 arguments 对象。调用第一个 getArgs 函数会抛出一个错误。可以使用rest参数来获得在箭头函数中传递的所有参数。

50.什么是类

类(class) 是在 JS 中编写构造函数的新方法。它是使用构造函数的语法糖,在底层中使用仍然是原型和基于原型的继承。

51.什么是模板字符串

在 JS 中创建字符串的一种新方法,可以通过使用反引号使模板字符串化。ES6中:

一行:

let greet = `Hi I am Mike!`;

多行:

let lastWord = `
    I
    am
    John
`;

在字符串中添加表达式或值:

function greet(name) {
    return `Hi ${name} !`;
}

52. 什么是对象解构?

对象解构是从对象或数组中获取值的一种方法,假设有如下的对象:

const employee = {
    firstName: "Marko",
    lastName: "Polo",
    position: "software Developer",
    yearHired: 2017
}

从对象获取属性:

{ firstName, lastName, position, yearHired } = employee;

还可以为属性取别名:

let { firstName: fName, lastName: lName, position, yearHired } = employee;

如果属性值为 undefined 时,可以指定默认值:

let { firstName = 'Mark', lastName: lName, position, yearHired } = employee;

53.展开(spread)运算符和剩余(Rest)运算符有什么区别?

展开运算符:’... ’可以将一个数组转为用逗号分隔的参数序列。展开元素会“展开”数组变成多个元素。

function add(a, b) {
    return a + b;
};
const nums = [5, 6];
const sum = add(...nums);

对 nums 数组进行展开,参数 a 的值是 5,b 的值是 6,sum 是 11

剩余运算符:’... ’用于解构数组和对象。收集多个元素和“压缩”成一个单一的元素

const [first, ...others] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(others); // [2, 3, 4, 5]

使用剩余操作符提取所有剩余的数组值,将它们放入除第一项之外的其他数组中

54.隐式和显式转换的区别

隐式强制转换是一种将值转换为另一种类型的方法,这个过程自动完成

console.log(1 + '6'); // 16

1被转换成字符串,然后与 + 运算符连接6

console.log(false + true); // 1

将 false 转换为 boolean 值为 0,true 为 1

console.log(6 * '2'); // 12

将 '2' 转换为一个数字,然后6 * 2

显式强制是将值转换为另一种类型的方法,需要手动转换

console.log(1 + parseInt('6'));

使用 parseInt 函数将 '6' 转换为 number,使用 + 运算符将 1 和 6 相加

55. 什么是NaN?以及如何检查值是否为NaN

NaN 表示非数字,该值是将数字转换或执行为非数字值的运算结果

let a;

JS 有一个内置isNaN 方法,用于测试值是否为isNaN值。

所有console.log 语句都返回 true,即使传递的值不是 NaN。在 ES6 中,建议使用 Number.isNaN 方法,因为它会检查该值(如果确实是 NaN)。

在 JS 中,NaN是唯一的值,它不等于自己。

56.判断值是否为数组

使用Array.isArray,如果环境不支持此方法,可以使用 polyfill 实现

还可以使用传统的方法

57.如何在不使用模运算符的情况下检查一个数字是否是偶数

使用按位 & 运算符,& 对其操作数进行运算,并将其视为二进制值执行与运算

58.如何检查对象中是否存在某个属性

使用 in 操作符号

使用 hasOwnProperty 方法,返回一个布尔值,指示对象自身属性中是否具有指定的属性

使用括号符号obj["prop"]。如果属性存在,返回该属性的值,否则返回undefined

59. in 运算符和 Object.hasOwnProperty 方法有什么区别

hasOwnPropert() 返回一个布尔值,指示对象自身属性中是否具有指定的属性,因此会忽略那些从原型链上继承的属性。在函数原型上定义一个变量 phone, hasOwnProperty 方法会直接忽略

in 运算符,如果指定的属性在指定的对象或其原型链中,返回 true

60. 函数表达式和函数声明有什么区别

notHoistedFunc 调用抛出异常,hoistedFunc 调用不会,因为 hoistedFunc 会被提升到作用域的顶部,而 notHoistedFunc 不会。

61.什么是缓存及它有什么作用?

缓存是建立一个函数的过程,这个函数能够记住之前计算的结果或值。使用缓存函数是为了避免在最后一次使用相同参数的计算中已经执行的函数的计算。这节省了时间,但也有不利的一面,会消耗更多的内存来保存以前的结果。

62.为什么typeof null 返回 object?如何检查一个值是否为 null

typeof null == 'object' 返回 true ,因为这是自 JS 诞生以来 null 的实现。

使用严格相等运算符 === 来检查值是否为 null

63.new 关键字的作用

1.创建空对象 {}

2.将空对象分配给 this 值

3.将空对象的 __proto__ 指向构造函数的 prototype

4.如果没有使用显式 return 语句,则返回 this

64.垃圾回收机制

在java中,程序员不需要自己去释放一个对象的内存,而是由虚拟机自行执行。在JVM中,垃圾回收线程是低优先级的,在正常情况下不执行,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,将它们添加到要回收的集合中进行回收。

65.location对象的常用属性和方法

什么是location对象?location对象的目的是获得或者设置URL

window对象提供了location,用于获取或设置窗体的URL,还可以用于解析URL。因为这个属性返回的是一个对象,所以将这个属性也称为location对象。

66.webpack 和 treeshaking

Tree-Shaking会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾被其它模块使用,并将其删除,以此实现打包产物的优化。

Webpack :所有模块的js代码混写在一起非常混乱,不具备代码层次化,难于调试,就进行模块的拆分,由于增加了js文件的数量,所以网页需要发起更多次的http请求,导致了页面加载速度变慢。无法直接在js代码中看出js文件之间相互的存储位置的关系,必须要通过dist.html文件才能查看。js文件必须按规定顺序加载。因此使用webpack进行文件打包,可以解决以上的缺点。

67.数组去重的方法

1.利用ES6中 Set去重

2.利用for双重循环,splice去重(双指针先后遍历数组,看是否有相等,相等则删除后者)

3.利用indexOf去重,新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组

4.利用includes

5.利用filter

68.vuex关键节点

vuex是一个专为 Vue.js 应用程序开发的状态管理模式, 采用集中式存储管理应用的所有组件的状态,解决多组件数据通信

1.state 统一定义管理公共数据

2.mutations: 使用它来修改数据

3.getters: 类似于vue中的计算属性

4.actions: 类似于methods,用于发起异步请求,比如axios

5.modules: 模块拆分

69.axios既可以当函数用,又可以当对象用Eg.axios({ }) 和 axios.get

Axios本质是函数,赋值了一些别名方法可被调用,最终调用的还是axios.prototype.request函数,最终返回的是promise链式调用

70. js中super关键字

super 关键字用于访问和调用一个对象的父亲的函数。。

在构造函数中使用时,super关键字将单独出现,并且必须在使用 this 关键字之前使用。super 关键字也可以用来调用父对象上的函数。

super(arguments);  // 调用父构造函数
  
super.parentMethod(arguments);  // 调用父方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

低保和光头哪个先来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值