Skip to content
On this page

JavaScript基础面试题汇总

1、JavaScript 的数据类型

JavaScript数据类型分为基本数据类型和引用类型。

  • 基本数据类型:String、Number、Boolean、Null、Undefined、Symbol(ES6)、Bigint(ES6)
  • 引用类型包括:Object、Array、Function

2、正则表达式的贪婪模式?

贪婪与非贪婪模式影响的是被量词修饰的子表达式的匹配行为:

  • 贪婪模式在整个表达式匹配成功的前提下,尽可能多的匹配;
  • 而非贪婪模式在整个表达式匹配成功的前提下,尽可能少的匹配;

属于贪婪模式的量词(匹配优先量词)

  • “{m,n}”“{m,}”“?”“*”“+”

在一些使用NFA引擎的语言中,在匹配优先量词后加上“?”,就会变成属于非贪婪模式的量词(忽略优先量词)

  • “{m,n}?”“{m,}?”“??”“*?”“+?”

3、如何判断JS变量的一个类型?

1、typeof

typeof返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、object、undefined、function、symbol(es6)等7种数据类型。

如果是判断一个基本类型typeof就是可以的。

js
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof null; //object 无效
typeof []; //object 无效
typeof new Function(); // function 有效
typeof new Date(); // object 无效
typeof new RegExp(); // object 无效

优点:对于所有基本的数据类型都能进行判断
缺点:不能判断其他复杂数据类型

2、instanceof

instanceof是用来判断 A 是否为 B 的实例对象。

在这里需要特别注意的是:instanceof检测的是原型

js
[] instanceof Array; //true
{} instanceof Object; //true
new Date() instanceof Date; //true

优点instanceof可以弥补Object.prototype.toString.call()不能判断自定义实例化对象的缺点
缺点instanceof只能用来判断对象类型,原始类型不可以。并且所有对象类型instanceof Object都是true,且不同于其他两种方法的是它不能检测出iframes

3、constructor

每一个对象实例都可以通过constrcutor对象来访问它的构造函数。JS中内置了一些构造函数:Object、Array、Function、Date、RegExp、String等。我们可以通过数据的constrcutor是否与其构造函数相等来判断数据的类型。

js
var arr = [];
var obj = {};
var date = new Date();
var num = 110;
var str = 'Hello';
var getName = function() {};
var sym = Symbol();
var set = new Set();
var map = new Map();

arr.constructor === Array; // true
obj.constructor === Object; // true
date.constructor === Date; // true
str.constructor === String; // true
getName.constructor === Function; // true
sym.constructor === Symbol; // true
set.constructor === Set; // true
map.constructor === Map // true

4、Object.prototype.toString.call()

toStringObject原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲,是toString运行时this指向的对象类型, 返回的类型格式为[object, xxx], xxx是具体的数据类型,其中包括:String, Number, Boolean, Undefined, Null, Function, Date, Array, RegExp, Error, HTMLDocument, ...基本上所有对象的类型都可以通过这个方法获取到。常用于判断浏览器内置对象。

js
Object.prototype.toString.call(''); // [object String]
Object.prototype.toString.call(1); // [object Number]
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(new Function()); // [object Function]
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call(new RegExp()); // [object RegExp]
Object.prototype.toString.call(new Error()); // [object Error]

function f(name) {
  this.name = name
}
var f1 = new f("martin")
console.log(Object.prototype.toString.call(f1)) //[object Object]

优点:对于所有基本的数据类型都能进行判断,即使是nullundefined
缺点:不能精准判断自定义对象,对于自定义对象只会返回[object Object]

5、Array.isArray()

Array.isArray()方法用于确定对象是否为数组,如果对象是数组,则次函数返回true,否则返回false

js
Array.isArray([1, 2, 3, 4])  // true
Array.isArray('1234')  // false

优点:当检测Array实例时,Array.isArray优于instanceof,因为Array.isArray可以检测出iframes
缺点:只能判别数组

6、Object.getPrototypeOf

Object.getPrototypeOf()静态方法返回指定对象的原型(即内部[[Prototype]]属性的值)。

js
Object.getPrototypeOf(a) === Array.prototype // true
Object.setPrototypeOf(a, Object.prototype);
Object.getPrototypeOf(a) === Array.prototype // false

缺点:原型是可以人为修改的

7、Array.prototype.isPrototypeOf()

还可以会用Array.prototype上的isPrototypeOf()方法来判断。

js
Array.prototype.isPrototypeOf([1, 2, 3]) // true

4、如何判断一个对象是否属于某个类?

js
class Person {}
var p = new Person()
// 方式一
if (p instanceof Person) {
  console.log("yes");
}

// 方式二
p.constructor === Person

// 方式三
Object.getPrototypeOf(p) === Person.prototype

// 方式四
p.__proto__ === Person.prototype

5、null和undefined的区别

nullundefined都是基本数据类型,它们分别都只有一个值,那就是nullundefined本身。

null:表示的含义是空对象,主要用于赋值给一些可能会返回对象的变量,作为初始化。

  • 使用typeof进行类型判断时,会返回object
  • null是JavaScript的关键字,是不允许用户用来作为标识符声明变量的;
  • null还是原型链的终点,通常null还用来释放内存
js
typeof null // object
var null = null // 报错:'null' is not allowed as a variable declaration name

undefined:表示的含义是未定义,一般变量声明了但没有定义的时候会返回undefined

  • 可以使用void 0的方式来获取安全的undefined
  • undefined不是JavaScript的关键字,因此可以用来作为变量名。
js
var a
console.log(a) // undefined
var undefined

特别注意: 两者转化成数字的值不同

js
Number(null) // 0
Number(undefined) // NaN
22 + null // 22
22 + undefined // NaN

null == undefined // true
null === undefined // false
!!null === !!undefined  // true

为什么typeof null返回的是object

因为不同的对象在底层都表现为二进制,在JavaScript中二进制前三位都为0的话会被判断为object类型,null的二进制全部都为0,前三位自然也是0,所以执行typeof值会返回object

6、=====的不同

  • ==运算符在比较之前会进行类型转换,如果两个值的类型不同就会尝试将它们转换为相同的类型,然后再进行比较;
  • ===运算符在比较之前不会进行类型转换。如果两个值的类型不同,那么它们就不相等。这被称为“严格相等”;

使用==时,可能发生一些特别的事情,例如:

js
1 == "1"; // true
1 == [1]; // true
1 == true; // true
0 == ""; // true
0 == "0"; // true
0 == false; // true

7、Object.is()=====的区别?

  • 使用==进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用===号进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回false
  • 使用Object.is来进行相等判断时,一般情况下和===的判断相同,它处理了一些特殊的情况,比如-0+0不再相等,两个NaN认定为是相等的。
js
+0 === -0; //true
NaN === NaN; // false

Object.is(+0, -0); // false
Object.is(NaN, NaN) // true

8、事件代理怎么实现?

事件代理是一种用于管理事件的技术,它利用事件冒泡机制,将事件监听器添加到父元素或祖先元素上,而不是直接添加到目标元素上。这样当事件在目标元素上触发时,它会冒泡到父元素,由父元素的事件监听器来处理。

9、事件委托是什么?

事件委托也称为事件代理或事件托管,它利用事件冒泡的原理,将目标节点的事件绑定到祖先节点上,利用父元素来代表子元素的某一类型事件的处理方式。

事件委托绑定的元素最好是被监听元素的父元素或祖先元素。因为事件冒泡的过程也需要时间,越接近顶层的元素,事件传播链就越长,耗时也就越多。

事件委托的优点包括:

  • 提高JS性能:通过事件委托,可以避免对每个子元素添加事件监听器,从而减少操作DOM节点的次数,减少浏览器的重绘和重排,进而提高代码的性能。例如当有一个包含大量元素的列表时,使用事件委托可以显著提高性能。
html
  <ul>
    <li>苹果</li>
    <li>香蕉</li>
    <li>凤梨</li>
  </ul>
  <script>
  // good
  document.querySelector('ul').onclick = (event) => {
    let target = event.target
    if (target.nodeName === 'LI') {
      console.log(target.innerHTML)
    }
  }
  // bad
  document.querySelectorAll('li').forEach((e) => {
    e.onclick = function() {
      console.log(this.innerHTML)
    }
  })
  </script>
  • 动态添加DOM元素:事件委托允许动态添加DOM元素,而不需要因为元素的变动而修改事件绑定。因为事件委托是基于事件冒泡机制,所以新添加的元素也会继承相同的事件处理逻辑。

注意事项:

  • 适合用事件委托的事件:clickmousedownmouseupkeydownkeyupkeypress
  • 虽然mouseovermouseout也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。
  • 不适合的就有很多了,例如mousemovefocusblur

案例:事件委托怎么取索引?

html
<ul id="ul">
  <li> aaaaaaaa </li>
  <li> 事件委托了 点击当前, 如何获取 这个点击的下标 </li>
  <li> cccccccc </li>
</ul>
js
window.onload = function() {
  var oUl = document.getElementById("ul");
  var aLi = oUl.getElementsByTagName("li");
  oUl.onclick = function(ev) {
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if (target.nodeName.toLowerCase() == "li") {
      var that = target;
      var index;
      for (var i = 0; i < aLi.length; i++) {
        if (aLi[i] === target) {
          index = i
        }
      }
      if (index >= 0) alert('我的下标是第' + index + '');
      target.style.background = "red";
    }
  }
}

10、事件捕获、事件冒泡、事件委托

DOM事件流指的是事件在DOM结构中传播的过程,当一个元素发生事件时,它的祖先和后代元素也可能会响应同样的事件。

事件传播的过程中分为三个阶段:

  • 事件捕获: 事件从祖先元素向下传播直到目标元素之前触发。在这一阶段中,事件从顶层元素开始逐级向下,直到达到目标元素的父元素为止。
  • 目标阶段: 事件到达目标元素,通过执行目标元素上的监听器函数响应事件。
  • 冒泡阶段: 事件从目标元素开始向上传播直到顶层元素触发。在这一阶段中,事件从目标元素的父元素开始逐级向上传递至顶层元素,直到达到顶层元素为止。 202302272001442.png

dom标准事件流的触发的先后顺序为:先捕获再冒泡,即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。

事件捕获

通俗的理解就是,当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。

事件冒泡

与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。

事件委托

又称事件代理,事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

案例:实现功能是点击li,弹出123,不使用事件委托实现。

html
<ul id="ul1">
  <li>111</li>
  <li>222</li>
  <li>333</li>
  <li>444</li>
</ul>
js
window.onload = function(){
  var oUl = document.getElementById("ul1");
  var aLi = oUl.getElementsByTagName('li');
  for(var i = 0; i < aLi.length; i++){
    aLi[i].onclick = function(){
      alert(123);
    }
  }
}

使用事件委托实现:

js
window.onload = function(){
  var oUl = document.getElementById("ul1");
  oUl.onclick = function(ev){
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if(target.nodeName.toLowerCase() == 'li'){
      alert(123);
      alert(target.innerHTML);
    }
  }
}

经典案例

202302272024251.png 可以看到,上面程序的输出结果:

我是 monther
我是 daughter
我是 baby
我是 grandma

造成这以结果的原因是:

target.addEventListener(type, listener, useCapture)

  • 参数一:事件类型,比如 click、mouseenter、drag等。
  • 参数二:事件被触发时的回调函数。
  • 参数三:useCapture: 默认值为false,表示在冒泡阶段处理事件。如果为true,则在捕获阶段处理事件

前面提到的DOM事件流的执行顺序是先捕获再冒泡,所以dom事件流从外向内捕获过程就是grandma -> monther -> daughter -> baby,而只有montherdaughter设置了useCapture = true,所以在捕获阶段就先将事件处理了,而grandmababy并未设置useCapture = true,默认是false,而我们又是点击的baby所以首先会先处理baby目标事件,然后再通过冒泡到grandma事件。

11、JS单线程还是多线程,如何显示异步操作

JS本身是单线程的,他是依靠浏览器完成的异步操作,而浏览器不是单线程的。

  • 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体步骤:

  1. 主线程执行js中所有的代码
  2. 主线程在执行过程中发现了需要异步的任务任务后扔给浏览器(浏览器创建多个线程执行),并在callback queue中创建对应的回调函数(回调函数是一个对象,包含该函数是否执行完毕等)
  3. 主线程已经执行完毕所有同步代码。开始监听callback queue一旦浏览器中某个线程任务完成将会改变回调函数的状态。主线程查看到某个函数的状态为已完成,就会执行该函数 202302141550108.png

12、微任务和宏任务

JavaScript是一门单线程语言,所以它本身是不可能异步的,但是JavaScript的宿主环境(比如浏览器、node)是多线程,宿主环境通过某种方式(事件驱动)使得js具备了异步的属性。

在js我们一般将所有的任务都分成两类:

  • 同步任务
  • 异步任务

而在异步任务中,又有着更加细致的分类:

  • 微任务
  • 宏任务

宏任务macrotask

宏任务是指那些被放入任务队列中需等待JavaScript引擎(主线程)的空闲时间才能执行的任务。

每当一个宏任务开始时,它都会执行宏任务队列中的一个任务。当宏任务执行完毕后,会查看微任务队列是否有任务,如果有就会执行所有的微任务。

宏任务是按照其在任务队列中的顺序依次执行的,当主线程执行完当前的所有同步任务后,会从任务队列中取出一个宏任务并执行它。

常见的宏任务包括:

  • script(整个代码脚本)
  • setTimeoutsetInterval
  • setImmediate(Node.js环境)
  • I/O操作(文件读写和网络请求等)
  • DOM事件
  • requestAnimationFrame(浏览器渲染的宏任务)
  • AJAX请求postMessageMessageChannel

微任务microtask

微任务是指那些在当前任务执行结束后立即执行的任务,它们可以被看作是在当前任务的“尾巴”上添加的任务。

微任务的执行优先级高于宏任务,即在一个宏任务结束后,会立即执行所有的微任务,然后再开始下一个宏任务。

微任务则是在当前宏任务执行过程中产生的(例如在同步代码执行过程中遇到Promise的状态变更),并且会在当前宏任务执行结束后立即执行,而不需要等待下一个宏任务开始。

常见的微任务包括:

  • Promise.thenPromise.catchPromise.finally
  • process.nextTick(Nodejs环境)
  • MutationObserver
  • Object.observe(已废弃)
  • async/await(基于Promise)

执行顺序

  • 当一个宏任务执行时,它内部的同步代码会直接执行,遇到异步代码(如setTimeout等宏任务的注册和Promise的创建及状态变更等)会根据其类型进行相应的处理:
    • 宏任务类型的异步代码会被添加到任务队列中等待后续执行。
    • 微任务类型的异步代码(如Promise的回调)会被添加到微任务队列中。
  • 当宏任务执行完毕后,会立即检查并执行微任务队列中的所有微任务,这个过程是一个连续的、依次执行的过程,直到微任务队列为空。
  • 只有当微任务队列清空后,才会开始下一个宏任务的执行。

示例代码

js
// 首先执行的是script任务,也就是全局任务,属于宏任务。
// script任务执行完后,开始执行所有的微任务
// 微任务执行完毕,再取任务队列中的一个宏任务执行
console.log('start');
setTimeout(() => {
  console.log('setTimeout');
}, 0)
new Promise((resolve, reject) => {
  for (var i = 0; i < 5; i++) {
    console.log(i);
  }
  resolve(6); // 修改promise实例对象的状态为成功的状态
}).then((res) => {
  console.log(res);
})
console.log('end');

// start 0 1 2 3 4 end 6 setTimeout

13、Promise构造函数是同步执行还是异步执行?

Promise构造函数是同步执行的,而Promise.then方法是异步执行的。

js
const promise = new Promise((resolve, reject) => {
  console.log(1)
  resolve()
  console.log(2)
})

promise.then(() => {
  console.log(3)
})

console.log(4)

输出结果:1 2 4 3

14、setTimeout、Promise、async/await的区别

setTimeout

setTimeout是JavaScript中的一个宏任务,它用于在指定的延迟后执行代码。

js
console.log('script start') //1. 打印 script start

setTimeout(function() {
  console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数

console.log('script end') //3. 打印 script start

// 输出顺序:
// ->script start
// ->script end
// ->settimeout

Promise

Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作,会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。

js
console.log('normal')
setTimeout(() => {
  console.log('setTimeout1')
})
let promise1 = new Promise((resolve) => {
  console.log('promise1')
  resolve()
  console.log('promise1 end')
}).then(function() {
  console.log('promise2')
})
setTimeout(function() {
  console.log('settimeout2')
})
console.log('normal end')
// 输出顺序: 
// normal
// promise1
// promise1 end
// normal end
// promise2
// settimeout1
// settimeout2

Promise中的then方法中的回调函数是微任务,在主线程同步任务执行完后依次执行。setTimeout属于宏任务,会在主线程的同步任务和Promise的微任务执行完之后,在下一个事件循环中执行,并且setTimeout队列里面的回调函数也是按照先后顺序执行的。

async/await

async/await是基于Promise的语法糖,使得异步代码看起来像同步代码一样易于理解和编写。async函数返回一个Promise对象,当函数执行到await时,它会暂停函数的执行并等待await后面的异步操作完成,然后再继续执行后面的代码。需要注意的是,await后面的表达式是同步执行的,但接下来的代码是异步的,属于微任务。

js
async function async1() {
  console.log('async1 start');
  await async2(); // 等待async2()完成,此时会继续执行主程序,输出script end,等async2()完成后,再回到async1()中继续执行剩下的代码
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start');
async1();
console.log('script end')

// 输出顺序:
// script start
// async1 start
// async2
// script end
// async1 end

async函数返回一个Promise对象,当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。

可以理解为,是让出了线程,跳出了async函数体。

js
// 举个例子
async function func1() {
  return 1
}
console.log(func1())

控制台查看打印,很显然,func1的运行结果其实就是一个Promise对象。因此我们也可以使用then来处理后续逻辑。

js
func1().then(res => {
  console.log(res); // 30
})

await的含义为等待,也就是async函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。

15、setTimeout和setImmediate以及process.nextTick的区别

process.nextTick()

  • process.nextTick是一个特殊的函数,用于将回调函数插入到事件循环的"next tick"队列中。它的回调函数具有最高的优先级,会在当前执行栈中的所有同步任务完成后立即执行。换句话说,任何通过process.nextTick调度的任务都会在同一个事件循环的其他异步任务之前执行。
  • process.nextTick通常用于执行那些需要尽快完成的任务,例如清理资源或同步更新状态。
js
process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){
    console.log(2);
  });
});

setTimeout(function C() {
  console.log(3);
}, 0)
// 1
// 2
// 3

当然这样也是一样的:

js
setTimeout(function C() {
    console.log(3);
}, 0)
process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){
    console.log(2);
  });
});
// 1
// 2
// 3

当然这样还是一样的:

js
setTimeout(function C() {
  console.log(3);
}, 0)
process.nextTick(function A() {
  process.nextTick(function B(){
    console.log(2);
  });
  console.log(1);
});
// 1
// 2
// 3

最后process.maxTickDepth()的缺省值是1000,如果超过会报exceed callback stack。官方认为在递归中用process.nextTick会造成饥饿event loop,因为nextTick没有给其他异步事件执行的机会,递归中推荐用setImmediate

js
foo = function(bar) {
  console.log(bar);
  return process.nextTick(function() {
    return f(bar + 1);
  });
};
setImmediate(function () {
  console.log('1001');
});
foo(1);//注意这样不会输出1001,当递归执行到1000次是就会报错exceed callback stack,

setTimeout()

  • setTimeout是一个用于设置在一定延迟后执行的定时器。它允许执行代码,但会在一定时间后将其插入事件队列。
  • setTimeout的回调函数将被插入到事件队列的定时器队列中。回调函数执行的时间不是精确的,而是在至少延迟指定时间后执行。如果在事件队列中存在其他阻塞操作,setTimeout的回调函数可能会延迟执行。
  • setTimeout适用于一般的异步操作和延迟执行。
js
setTimeout(function(){
  console.log('0')
}, 0);// 意思是回调函数加入事件队列的队尾,主线程和事件队列的函数执行完成之后立即执行定时器的回调函数,如果定时器的定时是相同的,就按定时器回调函数的先后顺序来执行。
console.log(1);
setTimeout(function(){
  console.log(2);
}, 1000);
setTimeout(function(){
  console.log(4);
}, 1000);
console.log(3);
//1 3 0 2 4

setImmediate()

  • setImmediate是一个用于安排立即执行的定时器。它在事件循环的检查阶段(check phase)执行,确保回调函数在I/O操作和定时器之后尽快执行。
  • setImmediate的回调函数将在事件队列的下一个检查阶段执行,优先级比setTimeout高,但低于process.nextTick
  • setImmediate适用于需要尽快执行的回调函数,尤其是在I/O操作之后。
js
console.log('1');

setImmediate(function () {
  console.log('2');
});

setTimeout(function () {
  console.log('3');
}, 0);

process.nextTick(function () {
  console.log('4');
});
//1 4 2 3也可能是1 4 3 2

16、JS运行机制(Event Loop)

JavaScript的运行机制基于事件循环(Event Loop),这是一个持续运行的过程,负责处理和执行JavaScript代码。

异步执行的机制如下:

  1. 所有同步任务都在主线程上执行,形成一个执行栈
  2. 主线程之外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些时间,那些对应的一步任务,于是结束等待状态,进行执行栈,开始执行
  4. 主线程不断重复上面的第三步

JavaScript的运行机制:主线程从任务队列中读取事件,这个过程是循环不断地,所以整个的这种机制又称为Event Loop(事件循环),只要主线程空了,就会读取任务队列,这个过程会循环反复。

17、对原型、原型链的理解?

原型

在JS中,我们所创建的每一个函数都自带一个属性prototype,它是一个对象。通过该函数实例化出来的对象都可以继承得到原型上的所有属性和方法。原型对象默认有一个属性constructor,值为对应的构造函数;

这里函数的prototype属性被称为显示原型,并且通过该函数实例化出来的对象还有一个__proto__,指向该函数的prototype属性,这里的__proto__又被称为隐式原型。 注意:虽说现在浏览器中都实现了__proto__属性来访问,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个Object.getPrototypeOf()方法,可以通过这个方法来获取对象的原型。

js
function Person(name) {
  this.name = name
}
const person = new Person('张三')
console.log(person.__proto__ === Person.prototype) // true
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(person.constructor === Person) // true
console.log(Person.prototype.constructor === Person) // true

var a = {
  name: '张三'
}
console.log(a.__proto__ === Object.prototype) // true
console.log(Object.getPrototypeOf(a) === Object.prototype) // true

原型链

当访问一个对象的属性时,会现在对象本身上查找,如果这个对象本身找不到这个属性,就会通过对象__proto__属性指向函数的原型对象(函数.prototype)一层一层往上找,直到找到Object的原型对象(Object.prototype)为止,层层继承的链接结构叫做原型链。

原型链的尽头一般来说都是Object.prototype所以这就是新建的对象为什么能够使用toString()等方法的原因。

注意项

由于Object是构造函数,原型链终点Object.prototype.__proto__,而Object.prototype.__proto__=== null // true,所以原型链的终点是null,原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__

2023021413594110.png202302141359533.png

18、prototype__proto__constructor的关系

  • __proto__被称为隐式原型
  • prototype被称为显式原型
  • 实例属性: 指的是在构造函数方法中定义的属性和方法,每一个实例对象都独立开辟一块内存空间用于保存属性和方法。
  • 原型属性: 指的是用于创建实例对象的构造函数的原型的属性,每一个创建的实例对象都共享原型属性。
  • 实例对象: 通过构造函数的new操作创建的对象是实例对象。
  • 原型对象: 构造函数有一个prototype属性,指向实例对象的原型对象。

1. 构造函数

构造函数: 用来初始化新创建的对象的函数。

js
function Foo(){};
var f1 = new Foo;

在例子中,Foo()函数是构造函数。

2. 实例对象

通过构造函数的new操作创建的对象是实例对象。

可以用一个构造函数,构造多个实例对象

js
function Foo(){};
var f1 = new Foo;
var f2 = new Foo;
console.log(f1 === f2);//false

3. 原型对象及prototype

构造函数有一个prototype属性,指向实例对象的原型对象。

通过同一个构造函数实例化的多个对象具有相同的原型对象。经常使用原型对象来实现继承。

js
function Foo(){};
Foo.prototype.a = 1;
var f1 = new Foo;
var f2 = new Foo;

console.log(Foo.prototype.a); // 1
console.log(f1.a); //1
console.log(f2.a); // 1
console.log(f1.__proto__ === f2.__proto__) // true

4. constructor

原型对象有一个constructor属性,指向该原型对象对应的构造函数

js
function Foo(){};
console.log(Foo.prototype.constructor === Foo);//true

由于实例对象可以继承原型对象的属性,所以实例对象也拥有constructor属性,同样指向原型对象对应的构造函数

js
function Foo(){};
var f1 = new Foo;
console.log(f1.constructor === Foo);//true

5. __proto__

实例对象有一个proto属性,指向该实例对象对应的原型对象。

js
function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype);//true

__proto__隐式原型

ES5使用Object.getPrototypeOf()方法来获取实例对象的原型对象。

js
function Demo() {}
const demo = new Demo()
Object.getPrototypeOf(demo) === Demo.prototype // true

一个对象的隐式原型指向该实例对象对应的原型对象(一个对象的隐式原型指向创建这个对象的函数的显式原型),这也保证了实例对象能够访问在构造函数原型中定义的属性和方法。

作用:构成原型链,同样用于实现基于原型的继承。

例子:当我们访问obj这个对象中的x属性时,如果在obj中找不到,那么就会沿着__proto__依次查找,直到最顶层,如果最顶层还未查找到则返回undefined

js
function Animal (name, age) {
  this.name = name
  this.age = age
}
const animal = new Animal('小三', 4)
console.log(animal.x) //undefined
Object.prototype.x = '123'
console.log(animal.x) // 123

Object.prototype这个对象是个例外,它的__proto__值为null

js
// ES5
function Animal (name, age) {
  this.name = name
  this.age = age
}
const animal = new Animal('小灰', 2)
console.log(animal.__proto__ === Animal.prototype) // true

// ES6
class Dog {
  constructor (name, age) {
    this.name = name
    this.age = age
  }
}
const dog = new Dog('小强', 3)
console.log(dog.__proto__ === Dog.prototype) // true

prototype显式原型

每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性指向函数的原型对象。

作用:用来实现基于原型的继承与属性的共享。

通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性。

js
function Animal (name, age) {
  this.name = name
  this.age = age
}
console.log(Animal.prototype) // {}
Animal.prototype.x = '123'
Animal.prototype.getX = function () {
  return this.x
}
console.log(Animal.prototype) // { x: '123', getX: [Function (anonymous)] }

const animal = new Animal('小三', 4)
console.log(animal.getX()) // 123

19、JavaScript 继承的方式和优缺点

原型链继承

原型链继承是指子类通过将自己的原型对象指向父类的实例来实现继承。

这种方式简单易懂,但是存在一些问题,如父类属性被所有子类共享、不能传递参数等。

js
function Animal (type) {
  this.type = type
}
Animal.prototype.say = function() {
  console.log('i am a ' + this.type)
}
function Cat (name) {
  this.name = name
}
Cat.prototype = new Animal('animal')

const cat = new Cat('cat')
console.log(cat.type, cat.name)  // animal cat
cat.say() // i am a animal

构造函数继承

构造函数继承是指在子类中调用父类的构造函数,使用callapply方法来继承父类的属性和方法。

但是,这种方式也存在问题,如无法继承父类的原型对象上的属性和方法。

js
function Animal (type) {
  this.type = type
}
Animal.prototype.say = function() {
  console.log('i am a ' + this.type)
}
function Cat (type) {
  Animal.call(this, type)
}

const cat = new Cat('cat')
console.log(cat.type)
cat.say() // TypeError: cat.say is not a function

组合继承

组合继承结合了原型链继承和构造函数继承的优点,既可以继承父类的属性和方法,又可以继承父类原型上的属性和方法,同时还可以向父类传递参数。

js
function Animal (type) {
  this.type = type
}
Animal.prototype.say = function () {
  console.log('i am a ' + this.type)
}
function Cat (type) {
  Animal.call(this, type)
}
Cat.prototype = new Animal('animal')
const cat = new Cat('cat')
console.log(cat.type) // cat
cat.say() // i am a cat

寄生组合式继承

寄生组合式继承是对组合继承的改进,避免了重复调用父类构造函数的问题,提高了效率。

js
function Animal (type) {
  this.type = type
}
Animal.prototype.say = function () {
  console.log('i am a ' + this.type)
}
function Cat (type) {
  Animal.call(this, type)
}
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat
const cat = new Cat('cat')
console.log(cat.type) // cat
cat.say() // i am a cat

拷贝继承

对父类实例中的的方法与属性拷贝给子类的原型

js
function Animal (type) {
  this.type = type
}
Animal.prototype.say = function () {
  console.log('i am a ' + this.type)
}
function Cat (type) {
  const animal = new Animal('animal')
  for (let key in animal) {
    Cat.prototype[key] = animal[key]
  }
  this.type = type
}
const cat = new Cat('cat')
console.log(cat.type) // cat
cat.say() // i am a cat

实例继承

为父类实例添加新特征,作为子类实例返回

js
function Animal (type) {
  this.type = type
}
Animal.prototype.say = function () {
  console.log('i am a ' + this.type)
}
function Cat (type) {
  const animal = new Animal('animal')
  animal.type = type
  return animal
}
const cat = new Cat('cat')
console.log(cat.type) // cat
cat.say() // i am a cat

class 继承

ES6 中引入了class关键字,可以更方便地实现继承。class继承本质上仍然是基于原型链的继承,只是语法更加简洁易懂。

js
class Animal {
  constructor(type) {
    this.type = type;
  }
  say() {
    console.log('i am a ' + this.type);
  }
}

class Cat extends Animal {
  constructor(type) {
    super(type);
  }
}
let cat = new Cat('cat');
console.log(cat.type); // cat
cat.say(); // i am a cat

20、对作用域、作用域链的理解?

作用域指的是变量和函数在代码中可见性和访问性的范围,即作用域指的是变量的可见区域

作用域是分层的,内层作用域可以访问外层作用域,反之不行。

其中作用域又分为全局作用域、函数作用域以及块级作用域。

全局作用域

  • 全局作用域在网页运行时创建,在网页关闭时销毁
  • 直接写到script标签中的代码都在全局作用域中
  • 所有window对象的属性拥有全局作用域
  • 全局作用域中的变量是全局变量,可在任意地方访问 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突
js
let a = 9
console.log(a) // 9
for (let i = 0; i < 2; i++) {
  console.log(a, i) // 9 0, 9 1
}
function demo () {
  console.log(a) // 9
}

函数作用域

函数作用域指在函数内部声明的变量,在函数内部函数内部声明的函数中都可以访问到。

  • 函数作用域在函数调用时创建,调用结束后销毁
  • 函数每一次调用都会产生一个全新的函数作用域,它们都是独立的,互不影响
  • 在函数作用域中声明的变量只能在块函数内部访问,外部访问不了
js
let a = 9
console.log(a) // 9

function demo () {
  let a = 10
  console.log(a) // 10
  console.log(b) // Uncaught ReferenceError: b is not defined
  return function test () {
    let b = 3
    console.log(a) // 10
    console.log(b) // 3
  }
}
demo()()

块级作用域

使用ES6中新增的letconst指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)

块作用域由{ }包括,if语句和for语句里面的{ }也属于块作用域

  • 使用let/const关键字创建的变量都具有块级作用域。
  • 块级作用域的变量只有在语句块内可以访问。所谓语句块就是用{ }包起来的区域。
  • 块级作用域有几个特性:不存在变量提升暂时性死区不允许重复声明
    • 什么是暂时性死区呢?
      只要块级作用域内存在let命令,它所声明的变量就绑定了这个区域,不再受外部影响。在代码块内,使用let命令声明函数之前,该变量都是不可用的,这在语法上称为“暂时性死区”
      js
      var tmp = 123;
      if (true) {
        tmp = 'abc'; // Uncaught ReferenceError: Cannot access 'tmp' before initialization
        let tmp;
      }
      
  • 通过var定义的变量可以跨块级作用域,而不能跨函数作用域
    js
    // if语句和for语句中用var定义的变量可以在外面访问到,
    // 可见,if语句和for语句属于块作用域,不属于函数作用域。
    f (true) {
      var c = 3;
    }
    console.log(c); // 3
    for (var i = 0; i < 4; i++) {
      var d = 5;
    };
    console.log(i); // 4  (循环结束i已经是4,所以此处i为4)
    console.log(d); // 5
    
  • 子作用域可以访问到父作用域的变量

作用域链

作用域链,用于解释代码中变量的访问规则。

当代码在作用域内访问一个变量时,JavaScript 引擎会先在当前作用域内查找该变量,如果找不到,就会逐级向上查找直到全局作用域,这个查找的过程就是作用域链,又称变量查找的机制

作用域链的作用: 保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的。

js
var a = 11
function demo () {
  let a = 1
  console.log(a)
}
demo() // 1

上面例子中,在demo中输出了变量a,首先会在demo的函数作用域找是否存在变量a,找到了就返回了变量a的值1

js
var a = 11
function demo () {
  console.log(a)
}
demo() // 11

上面例子中,在demo中输出了变量a,首先会在demo的函数作用域找是否存在变量a,没有找到,就向上级作用域(此处为全局作用域)查找,找到了就反悔了变量a的值11

js
function demo () {
  console.log(a)
}
demo() // Uncaught ReferenceError: a is not defined

上面例子中,同样首先在demo函数作用域查找变量a,没有找到,然后向上级作用域(全局作用域)查找没有找到,就会报Uncaught ReferenceError: a is not defined错。

21、变量提升、var、let、const的区别

变量提升

JS在执行之前,会先进行预编译,主要做两个工作(这就是变量提升):

  1. 将全局作用域或者函数作用域内的所有函数声明提前
  2. 将全局作用域或者函数作用域内的所有var声明的变量提前声明,并且复制undefined

变量的提升
使用var声明的变量,它会在所有代码执行前被声明,所以我们可以在变量声明前就访问变量,此时的值为undefined

js
console.log(a) // undefined
var a = 1
console.log(a) // 1

// 等价于
var a = undefined
console.log(a)
a = 1
console.log(a)

从下面例子可以看出:通过var定义的变量可以跨块作用域访问到。

js
if (true) {
  var a = 1
  console.log(a) // 1
}
console.log(a) // 1

// 等价于
var a = undefined
if (true) {
  a = 1
  console.log(a)
}
console.log(a)

从下面例子可以看出:通过var定义的变量不能跨函数作用域访问到。

js
(() => {
  var a = 1 // Uncaught ReferenceError: a is not defined
})();
console.log(a)

函数的提升
使用函数声明创建的函数function fn(){ },会在其他代码执行前先执行,所以我们可以在函数声明前调用函数。

js
demo()
function demo () {
console.log('aa') // aa
}

注意

  • 函数声明可以提升,但是函数表达式不提升,具名的函数表达式的标识符也不会提升;
  • 同名的函数声明,后面的覆盖前面的;
  • 函数声明的提升,不受逻辑判断的控制;
js
// 函数表达式和具名函数表达式标识符都不会提升
test(); // TypeError test is not a function
log(); // TypeError log is not a function
var test = function log() {
  console.log('test')
}

// 同名函数声明,后面的覆盖前面的
test(); // 2
function test() {
  console.log(1);
}
function test() {
  console.log(2);
}

// 函数声明的提升,不受逻辑判断的控制
// 注意这是在ES5环境中的规则,在ES6中会报错
function test() {
  log();
  if (false) {
    function log() {
      console.log('test');
    }
  }
}
test(); // 'test'

var、let、const的区别

  • var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问
  • var可以重复定义同一个变量,效果是重复赋值
js
var a = 1
var a = 2
console.log(a) // 2
  • let定义的变量,具有块级作用域,函数内部使用let定义后,对函数外部无影响,不能跨函数访问
  • let定义的变量不能重复定义同一个变量202302281614171.png
  • const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且定义之后是不允许改变的202302281612254.png
  • 同一个变量只能使用一种方式声明,不然会报错
js
var a = 1
let a = 2 // Uncaught SyntaxError: Identifier 'a' has already been declared
console.log(a)
  • var定义的全局变量会挂载到window对象上,使用window可以访问,let定义的全局变量则不会挂载到window对象上
js
var a = 1
console.log(window.a)

22、什么是属性搜索原则?

  1. 首先会去查找对象本身上面有没有这个属性,有的话,就返回这个属性
  2. 如果对象本身上面没有这个属性,就到它的原型上面去查找,如果有,就返回
  3. 然后又在原型的原型上面去查找有没有这个属性,如果查找到最后(Object.prototype)一直没有找到,就返回一个undefined

23、JavaScript中执行上下文和执行栈是什么?

执行上下文

执行上下文是指代码执行时的环境

每当JavaScript引擎需要执行一段代码时,就会创建一个对应的执行上下文,并将其放入执行上下文栈中。当这个执行上下文执行完成后,它会从栈中弹出,控制权再次回到上一个执行上下文。

执行上下文通常包括以下三个组成部分:

  1. 变量对象(Variable Object):用于存储当前环境中定义的变量和函数声明。
  2. 作用域链(Scope Chain):由当前执行上下文的变量对象和所有父级执行上下文变量对象的链式结构,用于实现词法作用域。
  3. this值:用于存储当前函数的上下文对象。

执行上下文可以分为三种类型:

  • 全局执行上下文:整个 JavaScript 代码的默认环境,由 JavaScript 引擎自动创建。
  • 函数执行上下文:每当一个函数被调用时,都会创建一个对应的函数执行上下文。
  • Eval执行上下文:在eval()函数中运行的代码块会在eval执行上下文中执行。

执行上下文是按照执行顺序逐层压入执行栈中的,栈顶的执行上下文表示当前正在执行的代码块。当代码块执行完成后,该执行上下文将从栈中弹出,控制权返回到上一级执行上下文。

执行栈

执行栈也称为调用栈(Call Stack),是一个后进先出(LIFO)的数据结构,用于跟踪当前正在执行的执行上下文。

每当一个执行上下文被创建时,它都会被推入执行栈的顶部,当执行上下文执行完毕后,它会被从执行栈中弹出,控制权转移到下一个执行上下文。另外每当一个函数被调用时,也会创建一个新的执行上下文,并将其压入执行栈的顶部。

例如,以下代码示例演示了执行栈中的执行顺序:

js
function add(a, b) {
  return a + b;
}
function multiply(a, b) {
  return a * b;
}
const result = multiply(3, add(2, 4));
console.log(result); // 输出 18
  • 在上述代码中,当multiply函数被调用时,会创建一个新的执行上下文并压入执行栈的顶部;接着,当add函数被调用时,也会创建一个新的执行上下文并压入执行栈的顶部。
  • add函数执行完成后,其对应的执行上下文将从栈中弹出,控制权返回到multiply函数,继续执行剩余的代码。当multiply函数执行完成后,其对应的执行上下文也将从栈中弹出,最终代码执行完毕,执行栈为空。

24、强制类型转换和隐式类型转换

强制类型转换

与隐式类型转换相反,强制类型转换需要手动进行,强制类型转换主要是通过调用全局函数来实现的,例如Number()、Boolean()、parseInt()、parseFloat()等。

js
Number("10.5") // 10.5
Number("10.5a") // NaN
Number(true) // 1
Number(false) // 0
Number(null) // 0

parseInt("123") // 123
parseInt('12b3') // 12

parseFloat("+3.12") // 3.12
parseFloat("-3.12") // -3.12
parseFloat(".12") // 0.12

隐式类型转换

表达式中包含以下运算符时,会发生隐式类型转换。

  • 算术运算符: +(加)、-(减)、*(乘)、/(除)、%(取模)
  • 逻辑运算符: &&(逻辑与)、||(逻辑或)、!(逻辑非)
  • 字符串运算符: ++=
js
'3' - 2 //  1
'3' + 2 // 32
3 + '2' // 32
3 - '2' // 1
'3' * '2' // 6
'10' / '2' // 5
1 + true // 2
1 + false // 1
1 + undefined // NaN
3 + null // 3
'3' + null // 3null
true + null // 1
true + undefined // NaN

25、你对闭包的理解?优缺点?

闭包是指有权访问另一个函数作用域中变量的函数,简单来说就是能够读取其他函数内部变量的函数

创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包可以用在许多地方,但是它的最大用处有两个:

  • 1、一个是前面提到的可以读取函数内部的变量;
  • 2、另一个就是让这些变量的值始终保持在内存中;

变量的值始终保存在内存中和读取函数内部的变量演示:

js
function demo () {
  let x = 1
  return function increment () {
    var y = 0
    console.log(++x)
    console.log(++y)
  }
}
const fn = demo()
fn() // 2 1
fn() // 3 1
fn() // 4 1

上面例子中,变量x的值始终保存在内存中,并且我们在increment函数中,确实也读取到了demo函数中的变量。

思考题

思考题一

js
var name = "The Window"
var object = {
  name: "My Object",
  getNameFunc: function () {
    return function () {
      return this.name
    }
  }
}
console.log(object.getNameFunc()())

上面输出The Window,为什么呢?

因为object.getNameFunc()()其实等价于下面两行代码

js
var result = object.getNameFunc() // 全局定义的变量其实就挂载在window对象上
console.log(result()) // 谁调用this就指向谁

思考题二

js
var name = "The Window"
var object = {
  name: "My Object",
  getNameFunc: function () {
    var that = this
    return function () {
      return that.name
    }
  }
}
console.log(object.getNameFunc()())

上面代码输出My Object,为什么呢?

因为这里的var that = this,此时的this就是我们的object,然后由于闭包会使得这些变量的值始终保持在内存中,所以当再次访问的时候that还是指向object,所以就输出My Object了。

26、如何判断NaN?

在JavaScript中,NaN(Not a Number)是一个特殊的值,它表示一个非数字的结果。NaN非常特殊,NaNNaN是不相等的。

isNaN()方法

js
let a = 1
let b = NaN
console.log('数字a:', typeof a, isNaN(a)) // 数字a: number false
console.log('数字b:', typeof(b), isNaN(b)) // 数字b: number true

使用isNaN()函数只能判断变量是否非数字,而无法判断变量值是否为NaN,于是可以使用方式二。

Number.isNaN()方法

函数Number.isNaN会首先判断传入参数是否为数字,如果是数字再继续判断是否为NaN,该函数只会在参数严格等于NaN时返回true

js
Number.isNaN(1) // false
Number.isNaN('aaaa') // false
Number.isNaN('1') // false
Number.isNaN(NaN) // true

27、isNaN和Number.isNaN函数的区别?

函数isNaN接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回true,因此非数字值传入也会返回true,会影响 NaN的判断。

js
isNaN(1) // false
isNaN('aaaa') // true
isNaN('1') // false
isNaN(NaN) // true

函数Number.isNaN会首先判断传入参数是否为数字,如果是数字再继续判断是否为NaN,这种方法对于NaN的判断更为准确。

js
Number.isNaN(1) // false
Number.isNaN('aaaa') // false
Number.isNaN('1') // false
Number.isNaN(NaN) // true

区别:

  • isNaN方法首先转换类型,而Number.isNaN方法不用;
  • isNaN不能用来判断是否严格等于NaN,Number.isNaN方法可用;

28、JS哪些操作会造成内存泄漏?

内存泄漏是指在代码执行期间,分配给对象的内存空间无法被垃圾回收机制释放,从而导致程序使用的内存不断增加,最终可能导致程序崩溃或性能下降的问题。

JS的垃圾回收机制

JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收系统(GC)会按照固定的时间间隔, 周期性的执行。

常见的引起内存泄露的原因:

  • 1、全局变量

    当全局变量中保存了对某个对象的引用时,这个对象就无法被垃圾回收机制释放。

    js
    var obj = { name: '张三' }
    
  • 2、意外的全局变量引起的内存泄露
    js
    // leak成为一个全局变量,不会被回收
    function leak() {
      leak = "xxx"
    }
    
  • 3、闭包引起的内存泄露

    闭包可以维持函数内局部变量,使其得不到释放。

    js
    function bindEvent() {
      var obj = document.createElement("XXX");
      obj.οnclick = () => {
        // obj的被缓存在内存中
      };
    }
    
  • 4、对DOM元素的引用未清除

    由于引用的存在,垃圾回收器无法回收该DOM元素占用的内存。

    js
    const elements = [];
    function addElement() {
      const element = document.createElement('div');
      elements.push(element);
      // 将元素添加到页面等操作
    }
    // 后续没有清理elements数组和其中的DOM引用的操作,即使相关元素已经从页面移除
    
  • 5、未清除的定时器

    定时器会一直触发回调函数,并且相关的资源和状态会一直保存在内存中,直到定时器被清除。

    js
    let intervalId = setInterval(() => {
      // 一些操作
    }, 1000);
    
  • 6、未清除的事件监听器

    在添加事件监听器时,如果在不需要的时候没有将其移除,就会导致内存泄露。随着页面的交互,会积累越来越多的事件监听器,它们占用的内存也不会被释放。

    js
    document.addEventListener('mousemove', function() {
      // 处理鼠标移动的逻辑
    });
    

29、谈谈垃圾回收机制方式及内存管理

垃圾回收机制:负责管理代码执行过程中使用的内存,垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

js
function fn1() {
  var obj = {name: 'hanzichi', age: 10};
}
function fn2() {
  var obj = {name:'hanzichi', age: 10};
  return obj;
}
var a = fn1();
var b = fn2();

fn1中定义的obj为局部变量,而当调用结束后,出了fn1的环境,那么该块内存会被js引擎中的垃圾回收器自动释放;在fn2被调用的过程中,返回的对象被全局变量b所指向,所以该块内存并不会被释放。

垃圾回收策略: 标记清除(较为常用)和引用计数

标记清除

当变量进入环境时,将变量标记"进入环境",当变量离开环境时,标记为:"离开环境"。某一个时刻,垃圾回收器会过滤掉环境中的变量,以及被环境变量引用的变量,剩下的就是被视为准备回收的变量。

到目前为止,IE、Firefox、Opera、Chrome、Safari的js实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。

引用计数

引用计数是跟踪记录每个值被引用的次数,就是变量的引用次数,被引用一次则加1,当这个引用计数为0时,被视为准备回收的对象。

内存管理

什么时候触发垃圾回收?

  • 垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。

IE6的垃圾回收是根据内存分配量运行的,当环境中的变量,对象,字符串达到一定数量时触发垃圾回收。垃圾回收器一直处于工作状态,严重影响浏览器性能。IE7中垃圾回收器会根据内存分配量与程序占用内存的比例进行动态调整,开始回收工作。

  • 合理的GC方案:
    • (1)、遍历所有可访问的对象;
    • (2)、回收已不可访问的对象。
  • GC缺陷:
    • 停止响应其他操作;
  • GC优化策略
    • (1)、分代回收(Generation GC);
    • (2)、增量GC

30、JavaScript严格模式和正常模式的区别

JavaScript的严格模式(strict mode)和正常模式(normal mode)的主要区别在于语法和行为的严谨性。严格模式使得JavaScript在更严格的条件下运行,旨在消除一些不合理、不严谨的语法,减少一些怪异行为,增加代码的安全性,提高编译器的效率。

严格模式使用"use strict",老版本的浏览器会把它当作一行普通字符串,加以忽略。

具体来说,严格模式主要有以下限制和变化:

  • 变量必须先声明后使用,且无法删除,不能重复声明

例如:use strict; a=1;var b = 2;会报错 Uncaught ReferenceError: a is not defined。同时delete b,在严格模式下也是错误的,如果尝试删除一个已经声明的变量,将会报错。

  • 严格模式中,函数形参存在同名的,抛出错误
js
'use strict'
function foo (p1, p1) {} // Uncaught SyntaxError: Duplicate parameter name not allowed in this context
  • 严格模式下,this的指向有所不同

正常模式下,this指向window对象,但在严格模式下,全局作用域中的函数的this指向undefined。而且如果构造函数不加new调用,this在严格模式下会指向undefined,赋值会报错,而在正常模式下,this会指向window全局对象。

  • 严格模式中,不允许八进制数和转义字符
js
'use strict'
var a = 010; //报错 Uncaught SyntaxError: Octal literals are not allowed in strict mode.
var a = \010; //报错 Uncaught SyntaxError: Invalid or unexpected token
  • 严格模式禁止了一些不安全的语法和行为

例如不能使用with语句,因为with语句在引用对象时不可预测,使得代码难以优化,且会拖慢代码的执行速度。

js
with (Object)
  statement
  • 严格模式下,函数的参数如果有重复,将会报错

例如,function foo(p1,p1){} 会报错,提示“Uncaught SyntaxError: Duplicate parameter name not allowed in this context”。

  • 严格模式中,不能对只读属性赋值,不能删除不可删除的属性
js
'use strict'
var obj = {};
obj.defineProperty(obj, 'a', {value:1, writable: false});//writable=false使属性不可写
obj.a = 2; //报错 Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'

delete Object.prototype //报错 Uncaught TypeError: Cannot delete property 'prototype' of function Object() { [native code] }
  • 严格模式中,eval当做关键字,不能被重新赋值和用作变量声明,在作用域eval()创建的变量不能被调用
js
var eval = 1; // 报错 Uncaught SyntaxError: Unexpected eval or arguments in strict mode
eval('var a = 1')
console.log(a) // 报错 Uncaught ReferenceError: a is not defined
  • 严格模式中,arguments当做关键字,不能被重新赋值和用作变量声明,不会自动反映函数参数的变化
js
var arguments = 1; // 报错 Uncaught SyntaxError: Unexpected eval or arguments in strict mode
  • 严格模式中,不能使用arguments.calleefn.callerfn.arguments
js
// 正常模式
function demo () {
  console.log(demo.arguments, arguments, arguments.callee) 
}
demo(1, 2) // [1, 2]  [1, 2]  ƒ demo () { console.log(demo.arguments, arguments, arguments.callee) }

// 严格模式报错
'use strict'
function demo () {
  console.log(demo.arguments, arguments, arguments.callee) 
}
demo(1, 2) // Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
  • 严格模式中,增加了publicprivateprotectedstaticletyieldpackageinterfaceimplements保留字
  • 严格模式中,callapply传入nullundefined保持传入的值
js
// 正常模式
function demo () {
  console.log(this)
}
demo.apply(undefined) // window

// 严格模式
'use strict'
function demo () {
  console.log(this)
}
demo.apply(undefined) // undefined

31、for in、for of、for 和 forEach 的区别

  • 1、for ... infor ... of都可以循环数组
    • for ... in输出的是数组的index下标
    • for ... of输出的是数组的每一项的值
js
const arr = [1,2,3,4]

// for ... in
for (const key in arr) {
  console.log(key) // 输出 0,1,2,3
}
// for ... of
for (const key of arr) {
  console.log(key) // 输出 1,2,3,4
}
  • 2、for ... in可以遍历对象,for ... of不能遍历对象,只能遍历带有iterator接口的,例如Set,Map,String,Array
js
const obj = {
  a: 1,
  b: 2,
  c: 3
}
for (let i in obj) {
  console.log(i)    //输出 : a   b  c
}
for (let i of obj) {
  console.log(i)    //输出: Uncaught TypeError: obj is not iterable 报错了
}
  • 3、forEach对数组的每一个元素执行一次提供的函数(不能使用returnbreak等中断循环),不改变原数组,无返回值。
    js
    let arr = ['a', 'b', 'c', 'd']
    arr.forEach(function (val, idx, arr) {
      console.log(val + ', index = ' + idx) // val是当前元素,index当前元素索引,arr数组
      console.log(arr)
    })
    // 输出结果
    // a, index = 0
    // (4) ["a", "b", "c", "d"]
    // b, index = 1
    // (4) ["a", "b", "c", "d"]
    // c, index = 2
    // (4) ["a", "b", "c", "d"]
    // d, index = 3
    // (4) ["a", "b", "c", "d"]
    
  • 4、for循环除了上三种方法以外还有一种最原始的遍历,自Javascript诞生起就一直用的,就是for循环,它用来遍历数组。for循环中可以使用returnbreak等来中断循环
    js
    var arr = [1,2,3,4]
    for(var i = 0 ; i< arr.length ; i++){
      console.log(arr[i])
    }
    

32、for in、Object.keys和Object.getOwnPropertyNames对属性遍历有什么区别?

  • for ... in会遍历自身及原型链上的可枚举属性
  • Object.keys会将对象自身可枚举属性的 key 输出
  • Object.getOwnPropertyNames会将自身所有属性的 key 输出

33、bind()、call()与apply()的作用与区别?

callapplybind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

apply

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

js
// 定义
fn.apply('this指向', [参数一, 参数二, 参数三, ... , 参数n])

返回值:使用调用者提供的this值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined

简单来说就是调用者那个方法fn的返回值,如果没有返回值就返回undefined。 例子:

js
function fn (...args) {
  console.log('this:', this)
  console.log('args:', args)
  return { name: '小黑', age: 12 }
}
const obj = {
  name: '张三'
}
const res = fn.apply(obj, ['小三', 24]) // this指向传入的obj 
console.log(res) // { name: '小黑', age: 12 }
// this: { name: '张三' }
// args: [ '小三', 24 ]
fn('小三', 24) // this指向window

当第一个参数为nullundefined的时候,默认指向window(在浏览器中)

html
<script>
  function fn (...args) {
    console.log('this:', this)
    console.log('args:', args)
  }
  fn.apply(null, ['小三', 24]) // this指向window
  fn.apply(undefined, ['小三', 24]) // this指向window
</script>

使用场景:在面向对象继承特点时会使用到,使子函数继承父函数的私有属性和方法(但原型对象的属性和方法不继承)

js
function Animal (name, age) {
  this.name = name
  this.age = age
  this.gender = 'male'
}
Animal.prototype.type = '狗类'

function Dog (name, age, color) {
  Animal.apply(this, [name, age]) // 这个来继承父类: 继承父函数的私有属性和方法, 但原型对象的属性和方法不继承
  this.color = color
}
const dog = new Dog('小灰', 3 , 'black')
console.log(dog) // Dog { name: '小灰', age: 3, gender: 'male', color: 'black' }

call

call方法的第一个参数也是this的指向,后面传入的是一个参数列表,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

js
// 定义
fn.call('this指向', 参数一, 参数二, 参数三, ... , 参数n)

返回值:使用调用者提供的this值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined

简单来说就是调用者那个方法fn的返回值,如果没有返回值就返回undefined。 例子:

js
function fn (...args) {
  console.log('this:', this)
  console.log('args:', args)
  return { name: '小黑', age: 12 }
}
const obj = {
  name: '张三'
}
const res = fn.call(obj, '小三', 24) // this指向传入的obj 
console.log(res) // { name: '小黑', age: 12 }
// this: { name: '张三' }
// args: [ '小三', 24 ]
fn('小三', 24) // this指向window

当第一个参数为nullundefined的时候,默认指向window(在浏览器中)

html
<script>
  function fn (...args) {
    console.log('this:', this)
    console.log('args:', args)
  }
  fn.call(undefined, '小三', 24) // this指向window
  fn.call(null, '小三', 24) // this指向window
</script>

使用场景:在面向对象继承特点时会使用到,使子函数继承父函数的私有属性和方法(但原型对象的属性和方法不继承)

js
function Animal (name, age) {
  this.name = name
  this.age = age
  this.gender = 'male'
}
Animal.prototype.type = '狗类'

function Dog (name, age, color) {
  Animal.call(this, name, age)
  this.color = color
}
const dog = new Dog('小灰', 3 , 'black')
console.log(dog) // Dog { name: '小灰', age: 3, color: 'black' }

bind

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入),改变this指向后不会立即执行,而是返回一个永久改变this指向的函数。

js
// 定义
const newFn = fn.bind('this指向', 参数一, 参数二, 参数三, ... , 参数n)

返回值:返回一个原函数的拷贝,并拥有指定的 this 值和初始参数(绑定函数(bound function简称BF)),调用绑定函数通常会导致执行包装函数绑定函数的返回值调用者方法fn的返回值,如果没有返回值就返回undefined。 例子:

js
function fn (...args) {
  console.log('this:', this)
  console.log('args:', args)
  return { name: '小黑', age: 12 }
}
const obj = {
  name: '张三'
}
const newFn = fn.bind(obj, '小三')
const res = newFn(24, 'red') // this指向传入的obj,args: ["小三", 24, "red"]
console.log(res) // { name: '小黑', age: 12 }
fn('小三', 24) // this指向window

当第一个参数为nullundefined的时候,默认指向window(在浏览器中)

html
<script>
function fn (...args) {
  console.log('this:', this)
  console.log('args:', args)
}
fn.bind(null, '小三', 24, 'red')() // this指向window
fn.bind(undefined, '小三', 24, 'red')() // this指向window
</script>

使用场景:在面向对象继承特点时会使用到,使子函数继承父函数的私有属性和方法(但原型对象的属性和方法不继承)

js
function Animal (name, age) {
  this.name = name
  this.age = age
  this.gender = 'male'
}
Animal.prototype.type = '狗类'

function Dog (name, age, color) {
  Animal.bind(this, name, age)()
  this.color = color
}
const dog = new Dog('小灰', 3 , 'black')
console.log(dog) // Dog { name: '小灰', age: 3, color: 'black' }

注意:绑定函数(bind函数返回的新函数)不可以再通过applycall改变其this指向,即当绑定函数调用applycall改变其this指向时,并不能达到预期效果。

js
var obj = {}
function test() {
    console.log(this === obj)
}
var testObj = test.bind(obj)
testObj()  //true

var objTest = {
    "作者": "chengbo"
}
/**
 * 预期返回false, 但是testObj是个绑定函数,所以不能改变其this指向
 */
testObj.apply(objTest) //true
testObj.call(objTest) //true

总结

从上面可以看到,applycallbind三者的区别在于:

  • 三者都可以改变函数的this对象指向;
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefinednull,则默认指向全局window;
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分为多次传入;
  • bind是返回绑定this之后的函数,applycall则是立即执行;

34、求数组的最大值或最小值

1、for循环

js
var a = [3, 1, 2, 3, 5];
let max = a[0]
let min = a[0]
for (let i = 0; i < a.length; i++) {
  if (a[i] > max) {
    max = a[i]
  }
  if (a[i] < min) {
    min = a[i]
  }
}
console.log(max, min) // 5 1

2、借助apply()call()方法

js
var a = [1, 2, 3, 5];
alert(Math.max.apply(null, a)); //最大值
alert(Math.max.call(null, ...a)); //最大值
alert(Math.min.apply(null, a)); //最小值
alert(Math.min.call(null, ...a)); //最小值

3、借助reduce()reduceRight()方法

js
var a = [3, 1, 2, 3, 5];
const max = a.reduce((total, currentValue, currentIndex, arr) => {
  if (currentValue >= total) {
    total = currentValue
  }
  return total
}, 0)
console.log(max)

4、借助sort()方法

js
var a = [3, 1, 2, 3, 5];
let max = a.sort((a, b) => b - a)[0]
console.log(max)

5、借助sort()reverse()方法

js
var a = [1, 2, 3, 5];
let max = arr.sort().reverse()[0];
console.log(max);

35、谈谈你对函数柯里化的理解?和偏函数的区别?

函数柯里化

柯里化(Currying)是一种函数转化方法,是把接受多个参数的函数变换成接受一个单一参数。

是高阶函数的一种: 接收函数作为参数的函数

常见的面试题: 用函数柯里化的方式实现一个函数,使add(1, 2, 3);add(1)(2, 3);add(1)(2)(3);返回的结果与

js
function add (x, y, z) {
  return x + y + z
}

的返回结果一致。

js
function add (x, y, z) {
  return x + y +z
}
function curry (fn, ...args) {
  if (args.length >= fn.length) {
    return fn(...args)
  }
  return (...otherArgs) => {
    return curry(fn, ...args, ...otherArgs)
  }
}
const addCurry = curry(add)
console.log(addCurry(1, 2, 3)) // 6
console.log(addCurry(1)(2, 3)) // 6
console.log(addCurry(1)(2)(3)) // 6

偏函数

偏函数就是将一个n参的函数转换成固定x参的函数,剩余参数n - x将在下次调用全部传入。

就和bind的使用方式一样,使用闭包实现

js
function partial(fn, ...args) {
  return (...arg) => {
    return fn(...args, ...arg)
  }
}
let partialAdd = partial((a, b, c) => a + b + c, 1)
console.log(partialAdd(2, 3)) // 6

36、target和currentTarget区别?

  • event.target:返回触发事件的元素;
  • event.currentTarget:返回绑定事件的元素;

currentTarget始终是监听事件者,而target是事件的真正发出者。

两者在没有冒泡的情况下,是一样的值,但在用了事件委托的情况下,就不一样了;

html
<ul id="ulT">
  <li class="item1">fsda</li>
  <li class="item2">ewre</li>
  <li class="item3">qewe</li>
  <li class="item4">xvc</li>
  <li class="item5">134</li>
</ul>
<script>
const ul = document.getElementById("ulT")
ul.addEventListener('click', (event) => {
  console.log(event.target, event.currentTarget);
})
</script>

202303211052231.png 可以通过设置事件在捕获过程触发,如下例子

html
<div class="father">
  <div class="child">child</div>
</div>
<script>
  const father = document.querySelector('.father')
  const child = document.querySelector('.child')
  child.addEventListener('click', (event) => {
    // 阻止冒泡
    console.log(event.target, event.currentTarget)
  }, true)
  father.addEventListener('click', (event) => {
    console.log(event.target, event.currentTarget)
  }, true)
</script>

点击child会发现控制台输出结果如下图所示,是因为我们在father的事件监听上设置了useCapture: true,即代表在捕获阶段触发,而事件查找是有外向内查找即html -> 目标元素的过程,所以就先触发了fatherclick事件。 202303211054585.png

37、JavaScript有几种类型的值?你能画一下他们的内存图吗?

分为两大类:

  • 栈: 原始数据类型(UndefinedNullBooleanStringNumber)
  • 堆: 引用数据类型(ArrayObjectFunction)

区别: 两大类存储位置不同。

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定,引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

    如果存储在栈中,将会影响程序运行的性能;

内存图如下所示: 202303021647274.png

38、iframe跨域通信和不跨域通信

主页面

html
<body>
  <iframe name="myIframe" id="iframe" class="" src="flexible.html" width="500px" height="500px">
  </iframe>
</body>
<script type="text/javascript" charset="utf-8">
  function fullscreen() {
    alert(1111);
  }
</script>

子页面flexible.html

html
<body>
  我是子页面
</body>
<script type="text/javascript" charset="utf-8">
  // window.parent.fullScreens()
  function showalert() {
    alert(222);
  }
</script>

不跨域通信

  1. 主页面要是想要调取子页面的 showalert 方法
js
myIframe.window.showalert();
  1. 子页面要掉主页面的 fullscreen 方法
js
window.parent.fullScreens();
  1. js 在 iframe 子页面获取父页面元素
js
window.parent.document.getElementById("元素id");
  1. js 在父页面获取 iframe 子页面元素代
js
window.frames["iframe_ID"].document.getElementById("元素id");

跨域通信

使用postMessage

  1. 子页面
js
window.parent.postMessage("hello", "http://127.0.0.1:8089");
  1. 父页面接收
js
window.addEventListener("message", function(event) {
  alert(123);
});

39、你对松散类型的理解

松散类型就是指当一个变量被申明出来就可以保存任意类型的值

简单来说就是在JavaScript中一个变量所保存值的类型是可以改变的,只是不推荐。

JavaScript 中变量可能包含两种不同的数据类型的值:基本类型和引用类型

  • 基本类型是指简单的数据段;
  • 引用类型指那些可能包含多个值的对象;

40、JavaScript数组常用方法

map

此方法是将数组中的每个元素调用一个提供的函数,结果作为一个新的数组返回,并不改变原数组

js
// map
// 作用:对数组进行遍历
// 返回值:新的数组
// 是否改变原有数组:不会
var arr = [2, 5, 3, 4];
var ret = arr.map(function(value) {
  return value + 1;
});
console.log(ret); //[3,6,4,5]
console.log(arr); //[2,5,3,4]

forEach

此方法是将数组中的每个元素执行传进提供的函数,没有返回值,直接改变原数组,注意和 map 方法区分

js
// forEach 方法
// 作用:遍历数组的每一项
// 返回值:undefined
// 是否改变原有数组:不会
var arr = [2, 5, 3, 4];
var ret = arr.forEach(function(value) {
    console.log(value); // 2, 5, 3, 4
});
console.log(ret); //undefined
console.log(arr); //[2,5,3,4]

reduce

此方法是所有元素调用返回函数,返回值为最后结果, 传入的值必须是函数类型,不改变原数组

js
// reduce 方法
// 作用:对数组进行迭代,然后两两进行操作,最后返回一个值
// 返回值:return出来的结果
// 是否改变原有数组:不会
var arr = [1, 2, 3, 4];
var ret = arr.reduce(function(a, b) {
  return a * b;
});
console.log(ret); // 24
console.log(arr); // [1, 2, 3, 4]

filter

此方法是将所有元素进行判断,将满足条件的元素作为一个新的数组返回,不改变原数组

js
// filter 过滤
// 作用: 筛选一部分元素
// 返回值: 一个满足筛选条件的新数组
// 是否改变原有数组:不会

var arr = [2, 5, 3, 4];
var ret = arr.filter(function(value) {
    return value > 3;
});
console.log(ret); //[5,4]
console.log(arr); //[2,5,3,4]

every

此方法是将所有元素进行判断返回一个布尔值,如果所有元素都满足判断条件,则返回 true,否则为 false,不改变原数组

js
let arr = [1, 2, 3, 4, 5]
const isLessThan4 = value => value < 4
const isLessThan6 = value => value < 6
arr.every(isLessThan4) // false
arr.every(isLessThan6) // true

some

此方法是将所有元素进行判断返回一个布尔值,如果存在元素都满足判断条件,则返回 true,若所有元素都不满足判断条件,则返回 false,不改变原数组

js
let arr = [1, 2, 3, 4, 5]
const isLessThan4 = value => value < 4
const isLessThan6 = value => value > 6
arr.some(isLessThan4) // true
arr.some(isLessThan6) // false

push

此方法是在数组的后面添加新加元素,此方法会改变原数组,并返回数组的长度:

js
let arr = [1, 2, 3, 4, 5]
arr.push(6) // 6
arr.push(6) // 7

pop

此方法在数组后面删除最后一个元素,此方法会改变原数组,并返回数组的长度:

js
let arr = [1, 2, 3, 4, 5]
arr.pop(6) // 5

shift

此方法在数组前面删除第一个元素,此方法会改变原数组,并返回第一个元素的值:

js
let arr = [2, 2, 3, 4, 5]
arr.shift() // 2

unshift

此方法是在数组后面删除最后一个元素,此方法会改变原数组,并返回最后一个元素的值:

js
let arr = [2, 2, 3, 4, 5]
arr.unshift() // 5

splice

语法:

js
arr.splice(开始位置, 删除的个数,元素)

万能方法,可以实现增删改:

js
let arr = [1, 2, 3, 4, 5];
let arr1 = arr.splice(2, 0 'haha')
let arr2 = arr.splice(2, 3)
let arr1 = arr.splice(2, 1 'haha')
console.log(arr1) //[1, 2, 'haha', 3, 4, 5]新增一个元素
console.log(arr2) //[1, 2] 删除三个元素
console.log(arr3) //[1, 2, 'haha', 4, 5] 替换一个元素

41、复杂数据类型如何转变为字符串

使用toString()方法

大多数内置的JavaScript对象都有toString()方法,用于将对象转换为字符串表示形式。

js
// 对于一个普通对象:
const obj = { name: 'John', age: 30 };
const strObj = obj.toString();
console.log(strObj); // 通常会输出类似于 "[object Object]" 的字符串

// 对于数组对象
const arr = [1, 2, 3];
const strArr = arr.toString();
console.log(strArr); // 输出 "1,2,3"

// 函数转换
function myFunction() {
  return "Hello";
}
const strFunction = myFunction.toString();
console.log(strFunction);
// 输出函数的定义字符串,如 "function myFunction() { return "Hello"; }"

使用JSON.stringify()方法

JSON.stringify()主要用于将JavaScript对象转换为JSON字符串格式。这在需要将复杂的对象结构转换为字符串以便进行网络传输或存储时非常有用。

js
const complexObj = {
  name: 'zhangsan',
  age: 23
};
const strComplexObj = JSON.stringify(complexObj);
console.log(strComplexObj); // { "name": "zhangsan", "age": 23 }

JSON.stringify()只能转换可序列化的对象属性,如果对象包含函数、循环引用或某些特殊类型的对象,可能会导致转换失败或结果不符合预期。

模板字符串

对于复杂对象,可以通过在模板字符串中调用对象的属性或方法来构建字符串。

js
const person = {
  name: 'Bob',
  greet: function() {
    return 'Hello!';
  }
};
const greeting = `${person.greet()} My name is ${person.name}.`;
console.log(greeting); // 输出 "Hello! My name is Bob."

42、观察者模式和发布订阅者模式

观察者模式

它定义了对象间的一种一对多的依赖关系,使得当一个对象状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。

js
// Subject: 被观察者
class Subject {
  constructor () {
    this.observers = []
  }
  add (observer) {
    this.observers.push(observer)
  }
  remove (observer) {
    const index = this.observers.findIndex(el => el === observer)
    if (index > -1) {
      this.observers.splice(index, 1)
    }
  }
  notify () {
    for (let i = 0 ; i < this.observers.length; i++) {
      this.observers[i].update()
    }
  }
}

// 观察者
class Observer {
  update () {
    console.log('update')
  }
}

// 创建一个被观察者
const subject = new Subject()
// 创建多个观察者
const observer1 = new Observer()
const observer2 = new Observer()
// 将观察者添加到被观察者的观察者列表中
subject.add(observer1)
subject.add(observer2)

// 通知所有观察者更新
subject.notify()

发布订阅模式

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

js
class EventBus {
  constructor () {
    this.hanlders = {}
  }
  // 订阅事件
  on (eventType, handle) {
    if (!this.hanlders[eventType]) {
      this.hanlders[eventType] = []
    }
    this.hanlders[eventType].push(handle)
  }
  // 发布事件
  emit (eventType, ...args) {
    if (this.hanlders[eventType]) {
      this.hanlders[eventType].forEach((item, index) => {
        item(...args)
      })
    }
  }
  // 取消订阅
  off (eventType, handle) {
    if (this.hanlders[eventType]) {
      this.hanlders[eventType].forEach((item, index, arr) => {
        if (item == handle) {
          arr.splice(index, 1)
        }
      })
    }
  }
}

const eventBus = new EventBus()
// 发布者
const publisher = {
  offwork (...args) {
    eventBus.emit('offwork', ...args)
  },
  onwork (...args) {
    eventBus.emit('onwork', ...args)
  }
}
// 订阅者
const subscriber = {
  onworkHandler (...args) {
    console.log('上班了', args)
  },
  offworkHandler (...args) {
    console.log('下班了', args)
  },
  onwork () {
    eventBus.on('onwork', this.onworkHandler)
  },
  offwork () {
    eventBus.on('offwork', this.offworkHandler)
  },
  cancelOffwork () {
    eventBus.off('offwork', this.offworkHandler)
  }
}

subscriber.onwork() // 订阅onwork
subscriber.offwork() // 订阅offwork
publisher.onwork('09:00', '记得打卡') // 上班了 ['09:00', '记得打卡']
publisher.offwork('18:00', '记得打卡') // 下班了 ['18:00', '记得打卡']
subscriber.cancelOffwork() // 取消订阅
publisher.offwork('18:00', '记得打卡') // 无输出

202302141606478.png

43、兼容各种浏览器版本的事件绑定

js
/*
  兼容低版本IE,ele为需要绑定事件的元素,eventName为事件名,fun为事件响应函数
*/
function addEvent(ele, eventName, fun) {
  // 看是否有addEventListener 
  if (ele.addEventListener) {
    // 大部分浏览器
    ele.addEventListener(eventName, fun, false);
  } else {
    // IE8及以下
    ele.attachEvent("on" + eventName, fun);
  }
}

44、DOMContentLoaded事件和Load事件的区别?

  1. 触发时间不同:DOMContentLoaded事件在HTML文档被完全加载和解析后触发,而Load事件在整个页面及其所有资源(如图片、样式表、脚本等)都加载完成后触发。
  2. 意义不同:DOMContentLoaded事件表示DOM树已经构建完成,可以进行操作,而Load事件则表示整个页面及其所有资源都已经完全加载完成,可以进行一些需要所有资源都准备就绪的操作,例如图像尺寸计算等。
  3. 响应速度不同:DOMContentLoaded事件响应速度较快,因为它只需要等待HTML文档加载和解析完毕即可触发,而Load事件需要等待整个页面及其所有资源加载完成后才能触发,因此响应速度较慢。
  4. 兼容性不同:DOMContentLoaded事件在大多数现代浏览器中都得到支持,而Load事件也具有广泛的兼容性,但在某些旧版本浏览器中可能存在兼容性问题。
html
<h1>DOMContentLoaded和load事件演示</h1>
<p>这是一个简单的演示,展示了在DOM树构建完成和所有资源加载完成时DOMContentLoaded和load事件的触发时间。</p>
<img src="https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885_960_720.jpg" alt="一棵树">
<script>
  document.addEventListener("DOMContentLoaded", function() {
    console.log("DOMContentLoaded事件: DOMContentLoaded事件触发时间为 " + new Date().getTime());
  });

  window.addEventListener("load", function() {
    console.log("load事件: load事件触发时间为 " + new Date().getTime());
  });
</script>

输出结果如下:

DOMContentLoaded事件: DOMContentLoaded事件触发时间为 1679649014340
load事件: load事件触发时间为 1679649024547

45、new操作符具体干了什么呢?

js
var Fn = function () {

};
var fnObj = new Fn();

new共经过了4几个阶段

  • 创建一个空对象
    js
    var obj = new object();
    
  • 设置原型链
    js
    obj.__proto__ = Fn.prototype;
    
  • Fn中的this指向obj,并执行Fn的函数体
    js
    var result = Fn.call(obj);
    
  • 判断Fn的返回值类型:判断Fn的返回值类型,如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。
    js
    if (typeof(result) === "object"){  
      return result;  
    } else {  
      return obj;
    }
    

46、如何判断当前脚本运行在浏览器还是node环境中?

浏览器端的window或者是node端的process全局对象。

方式一:通过判断Global对象是否为window,如果不为window,则当前脚本运行在node.js环境中。

js
this === window ? console.log('browser') : console.log('node');

方式二:通过判断process全局对象

js
typeof process === undefined ? console.log('browser') : console.log('node')

47、sort排序原理

sort排序使用的是冒泡排序法,其原理如下:

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
js
var arr = [1, 5, 4, 2];
// sort()方法的比较逻辑为:
// 第一轮:1和5比,1和4比,1和2比
// 第二轮:5和4比,5和2比
// 第三轮:4和2比
js
// 一.sort排序规则 return大于0则交换数组相邻2个元素的位置
// 二.arr.sort(function (a,b) {})中
//         a -->代表每一次执行匿名函时候,找到的数组中的当前项;
//         b -->代表当前项的后一项;

// 1.升序
var apple = [45, 42, 10, 147, 7, 65, -74];
// ①默认法,缺点:只根据首位排序
console.log(apple.sort());
// ②指定排序规则法,return可返回任何值
console.log(
  apple.sort(function(a, b) {
    return a - b; //若return返回值大于0(即a>b),则a,b交换位置
  })
);

//2.降序
var arr = [45, 42, 10, 111, 7, 65, -74];
console.log(
  apple.sort(function(a, b) {
    return b - a; //若return返回值大于零(即b>a),则a,b交换位置
  })
);

48、如何获取浏览器版本信息

js
window.navigator.userAgent
// 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'

49、自执行函数? 用于什么场景?好处?

自执行函数: 是指声明的一个匿名函数,可以立即调用这个匿名函数。作用是创建一个独立的作用域。

自执行函数又称立即调用函数立即执行函数,具有自执行,即定义后立即调用的功能。

js
// 写法一
(function(){
	console,log("hello world");
})();

// 写法二
(function(){
	console,log("hello world");
}());

一般用于框架、插件等场景。

简单的使用场景:for循环中通过延时器输出索引i

js
// 不使用立即执行函数
// 注意:这里需要使用var声明i,如果使用let,则不会导致变量提升
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);//输出:55555
  }, 1000);
}

// 使用立即执行函数
for (var i = 0; i < 5; i++) {
  ((index) => {
    setTimeout(() => {
      console.log(index)
    }, 1000)
  })(i);
}

好处

  • 防止变量弥散到全局,以免各种js库冲突;
  • 隔离作用域避免污染,或者截断作用域链,避免闭包造成引用变量无法释放;
  • 利用立即执行特性,返回需要的业务函数或对象,避免每次通过条件判断来处理;

50、多个页面之间如何进行通信

  1. 使用Cookies:可以在一个页面中设置Cookie值,在其他页面中读取该值来实现通信。
  2. 使用localStorage:通过监听storage的变化来实现通信;
  3. 使用postMessage()方法:可以在一个页面发送消息到另一个页面,并在那个页面中接收消息。
  4. 使用Broadcast Channel API:可以创建一个广播频道,在多个页面之间广播消息。
  5. 使用WebSocket:可以使用 WebSocket 连接在不同的页面之间建立实时通信。
  6. 使用web worker(SharedWorker): 可以使用SharedWorker的onmessagepostMessage进行通信。
  • 新建worker.js
    js
    // worker.js
    const set = new Set()
    onconnect = event => {
      const port = event.ports[0]
      set.add(port)
      // 接收信息
      port.onmessage = e => {
        // 广播信息
        set.forEach(p => {
          p.postMessage(e.data)
        })
      }
      // 发送信息
      port.postMessage("worker广播信息")
    }
    
  • 页面中使用
    html
    <!-- pageA -->
    <script>
      const worker = new SharedWorker('./worker.js')
      worker.port.onmessage = e => {
        console.info("pageA收到消息", e.data)
      }
    </script>
    
    <!-- pageB -->
    <script>
      const worker = new SharedWorker('./worker.js')
      let btnB = document.getElementById("btnB");
      let num = 0;
      btnB.addEventListener("click", () => {
        worker.port.postMessage(`客户端B发送的消息:${num++}`)
      })
    </script>
    

51、css动画和js动画的差异

css动画

优点

  • 1、浏览器可以对css动画进行优化
    浏览器可以对css动画进行优化,其优化原理类似于requestAnimationFrame,会把每一帧的DOM操作都集中起来,在一次重绘和回流中去完成。一般来说频率为每秒60帧。隐藏和不可见的dom不会进行重绘或回流,这样就会更少的使用CPU,GPU和内存使用量。
  • 2、强制使用硬件加速
    使用GPU来提高动画性能。
  • 3、代码相对简单,性能调优方向固定
  • 4、对于帧速不好的浏览器,css3可以做到自动降级,js则需要添加额外的代码

缺点

  • 1、无法控制中间的某一个状态,或者是给其添加回调函数,不能半路翻转动画,没有进度报告。
  • 2、如果想要实现相对复杂的动画效果时,css的代码冗余量很大。

js动画

优点

  • 1、js动画的控制能力很强,可以在动画播放过程中对动画进行控制,开始、暂停、回放、终止、取消都是可以做到的。
  • 2、动画效果比css3动画丰富,有些动画效果,比如曲线运动,冲击闪烁,视差滚动效果,只有JavaScript动画才能完成。
  • 3、CSS3有兼容性问题,而JS大多时候没有兼容性问题

缺点

  • 1、javascript在浏览器的主线程中运行,而主线程中还存在其他需要运行的javascript脚本,样式计算、布局、绘制任务等,对其干扰导致线程可能出现阻塞,从而造成丢帧的情况。
  • 2、js代码的复杂度要改与css动画

52、实现一个页面操作不会整页刷新的网站,并且能在浏览器前进、后退时正确响应

如果要实现页面操作不刷新网站,并且可以在浏览器中进行前进和后退操作,此时我们存在两个方法:

  • 一个是通过url的hash值操作
  • 另一个是通过HTML5的history方法。

url的hash方法

2023030514591610.png 在url中设置锚点,此时不会发生刷新效果,此时我们可以监听url的hash值的改变,然后进行请求数据,然后渲染页面即可,此时也是可以实现浏览器前进后退不刷新页面的效果的。

html
<div class="navbar">
  <a href="#/home">首页</a>
  <a href="#/about">关于</a>
  <a href="#/404">404</a>
</div>
<div id="app">
  default  
</div>
<script>
  const app = document.querySelector('#app')
  window.addEventListener('hashchange', () => {
    switch (location.hash) {
      case '#/home':
        app.innerHTML = 'Home'
        break
      case '#/about':
        app.innerHTML = 'About'
        break
      default:
        app.innerHTML = 'default'
    }
  })
</script>

如上面代码所示,此时我们当点击a链接时,此时改变urlhash值,此时我们可以通过监听urlhashchange方法,来执行相应的函数。

优点:hash值方法优势是兼容性好,在老版本的ie中可以运行,但是存在一个缺陷,就是存在#,显得url地址不真实。

HTML5的history api

html5中存在一些api,可以实现改变地址url但是不刷新页面。如果我们不使用html5中的api

html
<div class="navbar">
  <a href="#/home">首页</a>
  <a href="#/about">关于</a>
  <a href="#/404">404</a>
</div>
<div id="app">
  default  
</div>

此时点击a链接切换页面,此时会进行刷新。

此时对上面的标签设置相关的事件,执行相关的函数

html
<button id="btn">回退</button>
<div class="navbar">
  <a href="#/home">首页</a>
  <a href="#/about">关于</a>
  <a href="#/404">404</a>
</div>
<div id="app">
  default
</div>
<script type="module">
  const app = document.querySelector('#app')
  const links = document.querySelectorAll('a')

  // 循环为所有的a绑定点击事件
  for (let link of links) {
    link.addEventListener('click', (e) => {
      // 阻止默认行为
      e.preventDefault()
      let href = link.getAttribute('href')
      // 改变url地址,此时内容页面其他内容不发生改变
      history.pushState({}, '', href)
      // 当pushstate后,触发事件进行匹配
      renderView()
    })
  }

  // 需要在调用 back、go、forward时才会触发
  window.addEventListener('popstate', () => {
    console.log(123)
    renderView()
  })
  // 测试回退
  document.querySelector('#btn').addEventListener('click', () => {
    history.back()
  })

  function renderView () {
    switch (location.hash) {
      case '#/home':
        app.innerHTML = 'Home'
        break
      case '#/about':
        app.innerHTML = 'About'
        break
      default:
        app.innerHTML = 'default'
    }
  }
  renderView()
</script>

如果想要切换服务器数据,并且达到无刷新,可以在popstate监听函数中和a连接点击时触发ajax向服务器发起请求。

history 的 6个api总结

  • replaceState: 替换原来的路径。
  • pushState: 使用新的路径。
  • popState: 路径回退。
  • go: 向前或者向后。
  • back: 向后改变路径。
  • forward: 向前改变路径。

53、事件绑定与普通事件有什么区别

事件绑定相当于在一个元素上进行监听,监听事件是否触发。普通事件就是直接触发事件。

两者的区别:

在于是否可重复使用。事件绑定可以在一个元素上监听同一事件多次,而普通事件多次写会被覆盖。

普通事件

html
<!-- 普通添加事件的方法 -->
<button id="btn">按钮</button>
<script>
  const btn = document.querySelector('#btn')
  btn.onclick = () => {
    console.log('123')
  }
  btn.onclick = () => {
    console.log('234')
  }
</script>

点击按钮控制台会输出234

事件绑定

html
<!-- 事件绑定方式添加事件 -->
<button id="btn">按钮</button>
<script>
  const btn = document.querySelector('#btn')
  btn.addEventListener('click', () => {
    console.log('123')
  })
  btn.addEventListener('click', () => {
    console.log('234')
  })
</script>

点击按钮控制台会输出123 234

54、如何阻止冒泡与默认行为

  • 阻止冒泡行为:
    • 非 IE 浏览器stopPropagation()
    • IE 浏览器window.event.cancelBubble = true
  • 阻止默认行为:
    • 非 IE 浏览器preventDefault()
    • IE 浏览器window.event.returnValue = false

当需要阻止冒泡行为时,可以使用:

js
function stopBubble(e) {
  //如果提供了事件对象,则这是一个非IE浏览器
  if (e && e.stopPropagation) {
    //因此它支持W3C的stopPropagation()方法
    e.stopPropagation();
  } else {
    //否则,我们需要使用IE的方式来取消事件冒泡
    window.event.cancelBubble = true;
  }
}

当需要阻止默认行为时,可以使用

js
//阻止浏览器的默认行为
function stopDefault(e) {
  //阻止默认浏览器动作(W3C)
  if (e && e.preventDefault) {
    e.preventDefault();
  } else {
    // IE中阻止函数器默认动作的方式
    window.event.returnValue = false;
  }
  return false;
}

55、JavaScript的本地对象,内置对象和宿主对象

本地对象

本地对象(native object)与宿主无关,无论在浏览器还是服务器中都有的对象,就是ECMAScript标准中定义的类(构造函数)。在使用过程中需要我们手动new创建。

包括:BooleanNumberDateRegExpArrayStringObjectFunctionErrorEvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError

内置对象

内置对象(built-in object)与宿主无关,无论在浏览器还是服务器中都有的对象,ECMAScript已经帮我们创建好的对象,在使用过程中无需我们动手new创建。所有内置对象都是本地对象的子集。

包含:GlobalMathJSON

宿主对象

宿主对象(host object)由 ECMAScript 实现的宿主环境(如某浏览器)提供的对象(如由浏览器提供的WindowDocument),包含两大类,一个是宿主提供,一个是自定义类对象。所有非本地对象都属于宿主对象。

包含:WindowDocument、以及所有的DOMBOM对象以及localStorage等等。

什么是宿主?

宿主就是指JavaScript运行环境,js可以在浏览器中运行,也可以在服务器上运行(nodejs),对于嵌入到网页中的js来说,其宿主对象就是浏览器,所以宿主对象就是浏览器提供的对象。

56、内置函数(原生函数)

JavaScript 的内建函数(built-in function),也叫原生函数(native function)。

  • Number()
  • String()
  • Boolean()
  • Function()
  • Array()
  • Object()
  • Symbol()
  • Error()
  • Date()
  • RegExp()

原生函数可以被当作构造函数来使用,但是构造出来的值都是对象类型的:

js
var str = new String('hello world')
typeof str  // 'object'
Object.prototype.toString.call(str)  // '[object String]'

var str = 'hello world'
typeof str  // 'string'
Object.prototype.toString.call(str)  // '[object String]'

通过构造函数(如new String("hello world"))创建出来的是封装了基本类型值(如"hello world")的封装对象。注意的是

js
!!Boolean(false) // 结果: false;  第一步: Boolean(false) = false; 第二步: !!false = false
!!new Boolean(false) // 结果: true; 第一步: new Boolean(false)={false}; 第二步: !!{false} = true

57、JavaScript全局属性和全局函数有哪些?

JavaScript的全局属性和全局函数是由浏览器提供的,并且可以在全局作用域中直接访问。

全局属性

  • Infinity: 代表正的无穷大的数值。
  • -Infinity: 表示负无穷大。
  • NaN: 指示某个值是不是数字值。
  • undefined: 指示未定义的值。

全局函数

  • decodeURI(): 解码某个编码的URI
  • decodeURIComponent(): 解码一个编码的URI组件
  • encodeURI(): 把字符串编码为URI
  • encodeURIComponent(): 把字符串编码为URI组件
  • escape(): 对字符串进行编码
  • unescape(): 对由escape()编码的字符串进行解码
  • eval(): 计算 JavaScript 字符串,并把它作为脚本代码来执行
  • isFinite(): 检查某个值是否为有穷大的数
  • isNaN(): 检查某个值是否是数字
  • Number(): 将对象的值转换成数字
  • String(): 把对象的值转换为字符串
  • parseFloat(): 解析一个字符串并返回一个浮点数
  • parseInt(): 解析一个字符串并返回一个整数

其他全局对象

  • Math: 包含数学相关的常量和方法,如Math.PIMath.sqrt()等。
  • JSON: 包含处理JSON数据的方法,如JSON.parse()JSON.stringify()

58、escape, encodeURI, encodeURIComponent有什么区别?

JavaScript中有

  • 字符串编码的函数: escape, encodeURI, encodeURIComponent
  • 相应解码函数: unescape, decodeURI, decodeURIComponent

编码函数的区别

  • escape函数对字符串进行编码,将字符串中的非 ASCII 字符转义为十六进制转义序列。该函数现已被废弃,请勿再使用。
  • encodeURI函数对整个 URL 进行编码,将 URL 中的非 ASCII 字符和某些特殊字符转义为可安全传输的 ASCII 字符。但该函数不会对以下字符进行编码:;/?:@&=+$,#
  • encodeURIComponent函数对 URL 中的参数部分进行编码,将参数中的非 ASCII 字符和某些特殊字符转义为可安全传输的 ASCII 字符。该函数对所有非标准字符进行编码,包括! ' ( ) * 和 $等。
js
encodeURI("https://www.baidu.com?name=张三&age=23")
// 'https://www.baidu.com?name=%E5%BC%A0%E4%B8%89&age=23'
encodeURIComponent("https://www.baidu.com?name=张三&age=23")
// 'https%3A%2F%2Fwww.baidu.com%3Fname%3D%E5%BC%A0%E4%B8%89%26age%3D23'

console.log(escape("aaa12@*/+"))
//aaa12@*/+
console.log(escape("我是哈哈哈%"))
//%u6211%u662F%u54C8%u54C8%u54C8%25

59、两种函数声明有什么区别?

js
// 创建函数方式一: 函数表达式
var foo = function() {
  // Some code
};
// 创建函数方式二: 函数声明
function bar() {
  // Some code
};
// 创建函数方式三: 构造函数
const baz = new Function("console.log('aa')")

foo的定义是在运行时。想系统说明这个问题,我们要引入变量提升的这一概念。我们可以运行下如下代码看看结果。

js
console.log(foo)
console.log(bar)

var foo = function() {
  // Some code
};

function bar() {
  // Some code
};

输出结果为:

js
undefined
function bar(){ 
  // Some code
}; 

为什么那?为什么foo打印出来是undefined,而bar打印出来却是函数?

原因:JavaScript在执行时,会将变量提升。所以上面代码JavaScript 引擎在实际执行时按这个顺序执行。

js
// foo bar的定义位置被提升
function bar() {
  // Some code
};
var foo;

console.log(foo)
console.log(bar)

foo = function() {
  // Some code
};

函数声明赋值优先于变量声明赋值

  • 函数声明赋值,是在执行上下文的开始阶段进行的
  • 变量声明赋值,是在执行赋值语句的时候进行赋值的;
js
var getName = function() {
  console.log(4);
};

function getName() {
  console.log(5);
}
getName()

可以看到输出的结果是4

60、require与import的区别

importrequire都是被模块化所使用。在ES6当中,用export导出接口,用import引入模块。但是在node模块中,使用module.exports导出接口,使用require引入模块。

遵循规范不同

  • require是CommonJS/AMD规范;
  • import是ESMAScript6+规范;

遵循时间不同

  • require是运行时加载;
  • import是编译时加载;

由于编译时加载,所以import会提升到整个模块的头部;

调用位置不同

  • require可以在代码的任意位置调用;
  • import必须放在代码的顶部;

本质不同

  • require是赋值过程;

module.exports后面的内容是什么,require的结果就是什么。

  • import是解构过程;

加载方式

  • import是静态加载机制的;

这意味着在编译阶段,JavaScript引擎会分析和确定所有的import语句所依赖的模块,并构建出模块之间的依赖关系图,所以import只能在模块的顶层作用域中使用。

  • require是采用同步加载的方式;

另外还需要注意ES6提供了一个import()异步加载模块的方式,它相比于常规的import语句,import()允许在运行时动态地加载模块,从而实现按需加载和延迟加载的效果。import()返回一个Promise对象,可以使用then()方法获取导入的模块。

js
import('./foo.js')
  .then(module => {
    // 处理 module
  })
  .catch(error => {
    // 处理 error
  });

61、JavaScript中this的指向问题

一句话概括this指向:谁调用this就指向谁。优先级: 箭头函数 -> new -> bind -> call&apply -> obj.xx -> 直接调用 -> 不在函数里

箭头函数

箭头函数this的指向不会发生改变,也就是说在创建箭头函数时就已经确定了它的this的指向了;它的指向永远指向箭头函数外层的this

js
function fn1() {
  console.log(this);
  let fn2 = () => {
    console.log(this);
  }
  fn2(); // this->window
}
fn1();// this->window
// 因为fn1函数中this的指向是window,所以fn2箭头函数this指向fn1函数也就是间接指向window

直接调用(普通函数)

在函数被直接调用时,this将指向全局对象。在浏览器环境中全局对象是Window,在 Node.js 环境中是Global

  • 全局作用域中:this永远指向window,无论是否严格模式;
    js
    // 非严格模式
    console.log(this) // window
    
    // 严格模式
    'use strict';
    console.log(this) // window
    
  • 函数作用域中:
    • 如果函数直接被调用(函数名()),非严格模式this指向window,严格模式this指向undefined
    js
    // 简单例子
    function func() {
      console.log(this) // Window
    }
    func()
    
    // 严格模式
    'use strict';
    function func() {
      console.log(this) // undefined
    }
    func()
    
    js
    // 复杂的例子: 外层的 outerFunc 就起个迷惑目的
    function outerFunc() {
      console.log(this) // { x: 1 }
      function func() {
        console.log(this) // Window
      }
      func()
    }
    outerFunc.bind({ x: 1 })()
    
    • 被对象的对象.属性()调用,函数中的this指向这个对象,详细查看下节内容。

obj.xxx()

对象的对象.属性()调用,函数中的this指向这个对象

js
// 简单例子
var a = {
  fn () {
    console.log(this) // 指向a = { fn } 这个对象
  }
}
a.fn()

// 复杂例子1
var o = {
  prop: 37,
  f: function() {
    console.log(this.prop)
  }
};
function independent() {
  console.log(this.prop)
}
o.b = {
  g: independent,
  prop: 42
};
o.f() // 37
o.b.g() // 42

// 复杂例子2
var o = {
  a: 10,
  b: {
    // a:12,
    fn: function() {
      console.log(this.a); // undefined
      console.log(this); // {fn: ƒ}
    }
  }
};
o.b.fn();

// 复杂例子3
var o = {
  a: 10,
  b: {
    a: 12,
    fn: function() {
      console.log(this.a); // undefined
      console.log(this); // window
    }
  }
};
var j = o.b.fn;
j();

箭头函数中this不会被修改

js
var a = {
  fn: () => {
    console.log(this) // window
  }
}
a.fn()

obj.xxxbind一起使用

js
var obj = { name: 'obj' }
var a = {
  name: 'a',
  fn () {
    console.log(this) // obj
  }
}
a.fn.bind(obj)()

总结:this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的

new

当使用new关键字调用函数时,函数中的this一定是 JS 创建的新对象。

js
// 简单例子
function Person () {
  console.log(this) // Person {}
}
const person = new Person()

// 复杂例子1
function Person () {
  this.name = 'abc'
  return {
    name: 'cba' // 手动设置返回对象
  }
}
const person = new Person()
console.log(person.name) // cba

// 复杂例子2
function Person () {
  this.name = 'abc'
  return 1
}
const person = new Person()
console.log(person.name) // abc

// 复杂例子3
function Person () {
  this.name = 'abc'
  return undefined
}
const person = new Person()
console.log(person.name) // abc

// 复杂例子4
function Person () {
  this.name = 'abc'
  return null
}
const person = new Person()
console.log(person.name) // abc

总结: 如果手动设置的返回值是一个对象(null除外),那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。

那么有的人就会疑问,如果使用new关键调用箭头函数,是不是箭头函数的this就会被修改呢?

js
Person = () => {}
new Person() // Uncaught TypeError: Person is not a constructor

从控制台中可以看出,箭头函数不能当做构造函数,所以不能与new一起执行。

bind

bind是指Function.prototype.bind()。多次bind时只认第一次bind的值。

js
function fn() {
  console.log(this)
}
fn.bind(1).bind(2)() // 1

箭头函数中this不会被修改

js
fun = () => {
  // 这里 this 指向取决于外层 this
  console.log(this)
}
fun.bind(1)() // Window

易错点:bind 与 new 注意: new优先

js
function Person() {
  console.log(this, this.__proto__ === Person.prototype)
}
boundFunc = Person.bind(1)
new Person() // Person {} true

bind函数中this不会被修改

js
function func() {
  console.log(this)
}
 
boundFunc = func.bind(1)
// 尝试使用apply修改this
boundFunc.apply(2) // 1 -> 由结果可知bind的优先级高

apply和call

apply()call()的第一个参数都是this,区别在于通过apply调用时实参是放到数组中的,而通过call调用时实参是逗号分隔的。

js
function Person(name, age) {
  this.name = name
  this.age = age
  console.log(this)
}
const person = new Person('小明', 23) // Person {name: '小明', age: 23}
var obj = {}
Person.apply(obj, ['李四', 24]) // {name: '李四', age: 24}
Person.call(obj, '王五', 25) // {name: '王五', age: 25}
console.log(person) // Person {name: '小明', age: 23}

箭头函数中this不会被修改

js
func = () => {
  // 这里 this 指向取决于外层 this
  console.log(this)
}
func.apply(1) // Window

不在函数里

不在函数中的场景,可分为浏览器的script标签里,或 Node.js 的模块文件里。

  • script标签里,this指向Window
  • 在 Node.js 的模块文件里,this指向Module的默认导出对象,也就是module.exports

setTimeout和setInterval

  • 对于延时函数内部的回调函数的this指向全局对象window
  • 可以通过bind()方法改变内部函数this指向。
js
// 默认情况下代码
function Person() {
  this.age = 0;
  setTimeout(function() {
    console.log(this); // window
  }, 3000);
}

var p = new Person();

//通过bind绑定
function Person() {
  this.age = 0;
  setTimeout(
    function() {
      console.log(this); // Person{...}
    }.bind(this),
  3000);
}
var p = new Person(); 

62、对象浅拷贝和深拷贝有什么区别

对象的浅拷贝和深拷贝是指对于一个对象,将其复制一份后得到的新对象,两者的区别在于新对象与原对象之间是否共享引用的属性。

浅拷贝

浅拷贝简单来说就是只复制了原数据的内存地址,相当于两个数据指针指向了同一个内存地址,故只要其中任一数据元素发生改变,就会影响另一个。

换句话说,新对象中的引用类型属性仍然与原对象中的引用类型属性指向同一个内存地址。因此,在修改新对象中的引用类型属性时,会影响到原对象中的相应属性。

浅拷贝可以使用直接赋值Object.assignfor···in只循环一层扩展运算符来实现。

js
// 直接赋值
var obj1 = { a: 1, b: { c: 2 } };
var obj2 = obj1;
obj2.a = 3;
obj2.b.c = 4;
console.log(obj1); // {a: 3, b: {c: 4}}
console.log(obj2); // {a: 3, b: {c: 4}}

// Object.assign
var obj1 = {a: 1, b: {c: 2}};
var obj2 = Object.assign({}, obj1);
obj2.a = 3;
obj2.b.c = 4;
console.log(obj1); // {a: 1, b: {c: 4}}
console.log(obj2); // {a: 3, b: {c: 4}}

// 扩展运算符
var obj1 = { a: 1, b: { c: 2 } };
var obj2 = { ...obj1 };
obj2.a = 3;
obj2.b.c = 4;
console.log(obj1); // {a: 1, b: {c: 4}}
console.log(obj2); // {a: 3, b: {c: 4}}

在上述代码中,使用Object.assign方法实现了浅拷贝。当修改obj2的属性时,也会对obj1产生影响,因为它们共享了同一个 b 属性对象。

深拷贝

深拷贝就是指两个数据指向不同的内存地址,数据元素发生改变不会相互影响。

因此,在修改新对象中的引用类型属性时,不会影响到原对象中的相应属性。

常见的深拷贝方法可以使用JSON.stringifyJSON.parse两个函数来实现以及手写深拷贝

js
// JSON实现
var obj1 = {a: 1, b: {c: 2}};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.a = 3;
obj2.b.c = 4;
console.log(obj1); // {a: 1, b: {c: 2}}
console.log(obj2); // {a: 3, b: {c: 4}}

// 手写实现
function deepClone (target) {
  let res = Array.isArray(obj) ? [] : {};
  for (let key in target) {
    if (typeof target[key] === 'object') {
      res[key] = deepClone(target[key])
    } else {
      res[key] = target[key]
    }
  }
  return res
}
let obj1 = { name: '张三', age: 23 }
const obj2 = deepClone(obj1)
obj1.name = '王二'
obj2.name = '李四'
console.log(obj1) // {name: '王二', age: 23}
console.log(obj2) // {name: '李四', age: 23}

63、正则表达式构造函数RegExp与正则表达字面量/xxx/有什么不同?

正则表达式在JavaScript中可以通过两种方式创建:

  • 一种是使用正则表达式字面量(也称为正则表达式的直接量表示法);
  • 另一种是使用RegExp构造函数。

这两种方式在大多数情况下都可以正常工作,但它们之间确实存在一些细微的差异。

  • 语法差异
    • 正则表达式字面量:使用两个斜杠(/)来包围正则表达式,例如:/abc/
    • RegExp构造函数:使用new RegExp()来创建正则表达式,其中参数是一个字符串,表示正则表达式的模式。例如:new RegExp('abc')
  • 动态性
    • 正则表达式字面量在定义时就需要指定模式,且模式在编译时确定,不能动态更改。
    • RegExp构造函数允许动态地创建正则表达式。这意味着你可以在运行时确定正则表达式的模式,这在某些情况下可能非常有用。例如,你可以根据用户输入来构建一个正则表达式。
  • 转义字符
    • 在正则表达式字面量中,某些字符(如\/.*+?|()[]{}^)需要被转义(即前面需要加上\)。

    例如: 要匹配一个实际的点字符,你需要写/\./

    • 在RegExp构造函数中,你需要双重转义这些字符。

    例如: 同样的点字符需要写为new RegExp('\\.'),这是因为字符串字面量本身也会对这些字符进行转义。

这是一个关于转义字符的示例:

js
// 使用正则表达式字面量  
var literal = /\./;  
console.log(literal.test('a.b')); // true  
  
// 使用 RegExp 构造函数  
var constructor = new RegExp('\\.');  
console.log(constructor.test('a.b')); // true

64、JavaScript中callee与caller的作用

caller

返回一个调用当前函数的调用者,如果是由顶层调用的话则返回null

js
function demo () {
  console.log(demo.caller)
}
function nest () {
  demo()
}
demo() // null => 由于是顶层调用所以返回 null
nest() // ƒ nest () { demo() } => 由于是nest调用的demo,所以输出 nest 函数 

应用场景

主要用于察看函数本身被哪个函数调用。

callee

callee是对象的一个属性,该属性是一个指针,指向参数arguments对象的函数。返回正被执行的Function对象,也就是所指定的Function对象的正文。是对象的一个属性,

calleearguments的一个属性成员,它表示对函数对象本身的引用,这有利于匿名函数的递归或者保证函数的封装性。它有个length属性(代表形参的长度)。

js
function demo (name, age) {
  console.log(arguments.callee)
  console.log('实参长度:', arguments.length)
  console.log('形参长度:', arguments.callee.length)
}
demo('张三')
// 输出结果
// ƒ demo (name, age) {
//  console.log(arguments.callee)
//  console.log('实参长度:', arguments.length)
//  console.log('形参长度:', arguments.callee.length)
// }
// 实参长度: 1
// 形参长度: 2

demo('张三', 23)
// 输出结果
// ƒ demo (name, age) {
//  console.log(arguments.callee)
//  console.log('实参长度:', arguments.length)
//  console.log('形参长度:', arguments.callee.length)
// }
// 实参长度: 2
// 形参长度: 2

应用场景

一般用于匿名函数,有利于匿名函数的递归或者保证函数的封装性。

js
var fn = function (n) {
  if(n > 0) return n + fn(n-1);
  return 0;
}
console.log(fn(10)) // 55

函数内部包含了对自身的引用,函数名仅仅是一个变量名,在函数内部调用即相当于调用一个全局变量,不能很好的体现出是调用自身,这时使用callee会是一个比较好的方法

js
var fn = function (n) {
  if(n > 0) return n + arguments.callee(n - 1);
  return 0;
}
console.log(fn(10)) // 55

这样就让代码更加简练。又防止了全局变量的污染

65、异步加载js的方法

方案一: <script>标签的async="async"属性

点评:HTML5 中新增的属性,Chrome、FF、IE9&IE9+均支持(IE6~8 不支持)。此外,这种方法不能保证脚本按顺序执行。

方案二: <script>标签的defer="defer"属性

点评:兼容所有浏览器。此外,这种方法可以确保所有设置 defer 属性的脚本按顺序执行。

方案三: 动态创建<script>标签

点评:兼容所有浏览器。

html
<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript">
    (function() {
      var s = document.createElement("script");
      s.type = "text/javascript";
      s.src = "http://code.jquery.com/jquery-1.7.2.min.js";
      var tmp = document.getElementsByTagName("script")[0];
      tmp.parentNode.insertBefore(s, tmp);
    })();
  </>
</head>
<body>
  <img src="http://xybtv.com/uploads/allimg/100601/48-100601162913.jpg" />
</body>
</html>

方案四: 使用 AJAX 得到脚本内容,然后通过eval(xmlhttp.responseText)来运行脚本

点评:兼容所有浏览器。

方案五: iframe方式(这里可以参照:iframe 异步加载技术及性能 中关于 Meboo 的部分)

点评:兼容所有浏览器。

66、去除数组重复成员的方法(数组去重)

1、扩展运算符...Set结构相结合

js
// 去除数组的重复成员
[...new Set([1, 2, 2, 3, 4, 5, 5])];
// [1, 2, 3, 4, 5]

2、Array.fromSet结构结合

js
function dedupe(array) {
  return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]); // [1, 2, 3]

3、使用循环和indexof/lastIndexOf

js
function unique(arry) {
  const temp = [];
  arry.forEach(e => {
    if (temp.indexOf(e) == -1) {
      temp.push(e);
    }
  });
  return temp;
}

4、使用循环和includes|filter|find|some

js
let a = [1, 2, 1, 3, 4, 2]
let res = []
for (let i = 0; i< a.length; i++) {
  if (!res.includes(a[i])) {
    res.push(a[i])
  }
}
console.log(res) // [1, 2, 3, 4]

67、去除字符串里面的重复字符(字符串去重)

1、展开运算符...Set结构

js
[...new Set("ababbc")].join(""); // "abc"

2、使用ES5万能的reduce函数实现

js
var dic = {};
"ababbc".split("").reduce(function(total,next){
  if (!(next in dic)){
    dic[next] = true;
    total += next;
  }
  return total;
}, "")

3、使用正则表达式

js
var reg = /(.)(?=.*\1)/g;
var result = 'ababbc'.replace(reg, "");
console.log(result)

68、innerHTML与outerHTML的区别?

  • innerHTML: 包含HTML元素的子元素、文本节点以及它们的属性;

    可以用于动态地添加或更改 DOM 元素的内容,也可以用于获取元素的 HTML 内容作为字符串。

  • outerHTML: 包含HTML元素本身以及它的子元素、文本节点以及它们的属性;

    可以用于动态地替换整个 DOM 元素,也可以用于获取元素的 HTML 内容作为字符串。

对于这样一个 HTML 元素:

html
<div id="box">content<span>span</span></div>
  • innerHTML:内部HTML,content<span>span</span>
  • outerHTML:外部 HTML,<div id="box">content<span>span</span></div>

69、节流throttling和防抖debouncing是什么?

防抖(debouncing)

防抖指的是触发某个事件后,等待一段时间(设置定时器),如果这段时间内没有再次触发该事件,则执行相应的代码,否则重新开始等待一段时间。

例如: 当用户不断地输入搜索关键字时,我们可以设置一个输入框的防抖函数,只有用户停止输入一段时间后才触发搜索操作,而不是在每次输入时都进行搜索。

js
// 防抖函数
function debounce(callback, time) {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      callback.apply(this, args);
    }, time);
  };
}

测试代码

html
<el-input v-model="value" @input="handleInput" />
<script setup lang="ts">
import { ref } from 'vue';
const value = ref('')
function handleInput (value) {
  console.log(value)
  debounce(() => {
    console.log('执行请求', value)
  }, 2000)()
}
// ...
</script>

可以看到我们在input框中输入内容后,如果两秒后没有输入内容,则才会执行callback

节流throttling

节流指触发某个事件后,等待一段时间(设置定时器),如果这段时间内没有再次触发该事件,则执行相应的代码,如果这段时间内有再次触发,则直接忽略,直到等待时间结束后才能执行相应的代码或再次触发事件。

例如: 当用户不断地滚动页面时,我们可以设置一个滚动事件的节流函数,使得在每个固定时间间隔内只执行一次滚动处理函数,而不是每滚动一下就执行一次。

js
function throttle(callback, delay) {
  let timer;
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        callback.apply(this, arguments);
        timer = null;
      }, delay);
    }
  };
}

70、如何判断一个对象是否为空对象?

  1. 使用Object.keys()方法获取该对象所有的键名,然后判断数组长度是否为0
js
function isEmpty(obj) {
  return Object.keys(obj).length === 0;
}
  1. 使用for...in循环遍历该对象,如果循环体被执行了一次,则该对象不是空对象。
js
function isEmpty(obj) {
  for (let key in obj) {
    return false;
  }
  return true;
}
  1. 以上两种方法都有一个潜在的问题,即如果该对象原型链上有属性,则会被算作该对象的属性。如果需要只判断该对象自身的属性是否为空,请使用Object.getOwnPropertyNames()方法或Object.getOwnPropertySymbols()方法。例如:
js
function isEmpty(obj) {
  return (
    Object.getOwnPropertyNames(obj).length === 0 &&
    Object.getOwnPropertySymbols(obj).length === 0
  );
}
  1. 当然可以使用JSON.stringify方法来判断:
js
if (JSON.stringify(obj) === '{}') {
  console.log('空对象')
}

71、使用闭包实现每隔一秒打印1, 2, 3, 4

js
// 使用闭包实现
for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}

// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}

72、Set 和 WeakSet 结构?

  1. ES6 提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
  2. WeakSet结构与Set类似,也是不重复的值的集合。但是WeakSet的成员只能是对象,而不能是其他类型的值。WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用。
js
// Set
const set = new Set()
set.add(1).add(2).add(2); // 注意2被加入了两次

set.size // 2

set.has(1) // true
set.has(2) // true
set.has(3) // false

set.delete(2) // true
set.has(2) // false

// WeakSet
const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
ws.add([1, 2]) // 正确

73、Map 和 WeakMap 结构?

  1. Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
  2. WeakMap结构与Map结构类似,也是用于生成键值对的集合。但是WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。而且 WeakMap的键名所指向的对象,不计入垃圾回收机制。
js
// Map
const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

map.set(1, 'aaa').set(1, 'bbb');
map.get(1) // "bbb"

map.delete(1)
map.get(1) // undefined

map.clear()
map.size // 0

// WeakMap
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2

// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"

const wm = new WeakMap();
wm.set(1, 2)
// TypeError: 1 is not an object!
wm.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
wm.set(null, 2)
// TypeError: Invalid value used as weak map key
let key = {};
let obj = {foo: 1};
wm.set(key, obj) // 正确

74、异步编程的实现方式?

  • 回调函数(Callback):这是JavaScript中最基本的异步编程方式。当异步操作完成时,会调用一个函数(回调函数)来处理结果。然而,当有大量异步操作需要处理时,可能会出现所谓的"回调地狱"(Callback Hell)。
js
fs.readFile('/path/to/file', 'utf8', function(err, data) {  
  if (err) {  
    console.error(err);  
    return;  
  }  
  console.log(data);  
});
  • Promise:Promise是一个代表异步操作最终完成或失败的对象。它解决了回调地狱的问题,使得异步代码更加清晰和易于管理。
js
let promise = new Promise((resolve, reject) => {  
  // 异步操作  
  setTimeout(() => resolve("成功!"), 1000);  
});  
  
promise.then(  
  result => console.log(result), // 成功时的回调函数  
  error => console.log(error)    // 失败时的回调函数  
);
  • async/await:async/await是基于Promise的语法糖,使得异步代码看起来更像同步代码,更易于理解和编写。
js
async function asyncFunc() {  
  try {  
    let result = await someAsyncOperation();  
    console.log(result);  
  } catch (error) {  
    console.error(error);  
  }  
}  
asyncFunc();
  • 事件监听(Event Listener):在Node.js中,许多对象都是EventEmitter的实例,可以发出和监听事件。这种方式在浏览器端的JavaScript中也很常见,如处理用户输入、网络请求等。
js
const EventEmitter = require('events');  
const eventEmitter = new EventEmitter();  
// 监听事件  
eventEmitter.on('someEvent', function(arg1, arg2) {  
  console.log(`Event triggered with ${arg1} and ${arg2}`);  
});  
// 触发事件  
eventEmitter.emit('someEvent', 'arg1', 'arg2');
  • Generator函数:Generator 函数是一种特殊类型的函数,它可以在执行过程中被暂停和恢复。这使得 Generator 函数非常适合处理异步操作,因为它们可以在等待异步操作完成时暂停执行,然后在异步操作完成后恢复执行。
js
function* asyncGenerator() {  
  let result = yield fetch('https://api.example.com/data');  
  console.log(result.data);  
}  
  
const generator = asyncGenerator();  
  
generator.next().value.then(data => {  
  generator.next(data);  
});

75、异步编程的实现方式?

  • 回调函数(Callback)
    js
    function getData(callback) {
      // 模拟异步请求数据
      setTimeout(() => {
        const data = { name: 'John', age: 30 };
        callback(data);
      }, 1000);
    }
    // 调用getData函数,并传入回调函数处理返回的数据
    getData(data => {
      console.log(data.name); // 输出John
    });
    
  • Promise
    js
    function getData() {
      return new Promise((resolve, reject) => {
        // 模拟异步请求数据
        setTimeout(() => {
          const data = { name: 'John', age: 30 };
          resolve(data);
        }, 1000);
      });
    }
    // 调用getData函数,使用then方法处理返回的数据
    getData().then(data => {
      console.log(data.name); // 输出John
    }).catch(error => {
      console.error(error);
    });
    
  • 异步函数/Async Await
    js
    function getData() {
      return new Promise((resolve, reject) => {
        // 模拟异步请求数据
        setTimeout(() => {
          const data = { name: 'John', age: 30 };
          resolve(data);
        }, 1000);
      });
    }
    // 使用async关键字声明异步函数,使用await关键字等待Promise对象的结果
    async function main() {
      try {
        const data = await getData();
        console.log(data.name); // 输出John
      } catch (error) {
        console.error(error);
      }
    }
    main();
    
  • 事件监听/发布订阅模式
    js
    // 创建一个事件监听器对象
    const eventEmitter = new EventEmitter();
    // 绑定事件处理函数
    eventEmitter.on('dataReceived', data => {
      console.log(data.name); // 输出John
    });
    function getData() {
      // 模拟异步请求数据
      setTimeout(() => {
        const data = { name: 'John', age: 30 };
        // 发送事件,通知数据已经接收完成
        eventEmitter.emit('dataReceived', data);
      }, 1000);
    }
    // 调用getData函数,在接收到数据后发送事件
    getData();
    
  • Generator函数和yield关键字
    js
    function* getData() {
      const data = yield new Promise(resolve => {
        // 模拟异步请求数据
        setTimeout(() => {
          resolve({ name: 'John', age: 30 });
        }, 1000);
      });
      return data;
    }
    // 创建一个生成器对象
    const generator = getData();
    // 调用生成器的next方法,执行到第一个yield语句
    generator.next().value.then(data => {
      // 将Promise的结果传入生成器,并继续往下执行
      generator.next(data);
    }).then(result => {
      console.log(result.name); // 输出John
    });
    

76、谈谈对Generator的理解?

Generator是ES6引入的一种特殊的函数类型,它可以暂停执行并在需要的时候恢复执行,实现了一种可控的、逐步执行的函数流程。Generator函数的定义使用 function*语法声明,并且在函数内部可以使用yield关键字来暂停函数的执行并返回一个中间值。

js
function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

在这个简单的Generator函数myGenerator中,每次调用yield都会暂停函数的执行,并将其后的值返回给调用者。

Generator函数返回的是一个迭代器对象,可以通过调用next()方法来逐步执行函数中的代码。每次调用next()方法,函数会从上次暂停的位置继续执行,直到遇到下一个yield表达式或者函数结束。 next()方法返回一个对象,包含两个属性:value(表示当前yield表达式的值)和done(表示函数是否已经执行完毕,true表示执行完毕,false表示仍在执行中)。

js
const generator = myGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }

优点

  • 改善异步编程体验:使异步代码更易于编写和理解,尤其是对于复杂的异步流程,可以避免回调地狱和复杂的 Promise 链。
  • 灵活的控制流:可以根据需要暂停和恢复执行,实现更加精细的流程控制,适用于各种需要逐步处理数据或异步操作的场景。
  • 可迭代特性:可以方便地创建可迭代的数据序列,用于实现自定义的迭代器和数据生成器。

缺点

  • 浏览器兼容性问题:在一些旧版本的浏览器中可能不被支持,需要进行适当的转译或兼容处理。
  • 性能开销:由于其特殊的执行机制和额外的状态管理,相比普通函数可能会有一定的性能开销,尤其是在频繁调用和复杂的场景下。
  • 学习曲线:对于不熟悉函数式编程概念和异步流程控制的开发者来说,理解和正确使用 Generator 可能需要一定的学习成本。

77、谈谈对Promise的理解?

Promise是异步编程的一种解决方案,它是一个用于处理异步操作的一种对象。它代表了一个尚未完成但预期在未来会完成的操作及其结果。Promise 有三种状态:

  • Pending(进行中):初始状态,表示异步操作正在进行中。
  • Fulfilled(已成功):表示异步操作成功完成,此时Promise会返回一个值(通常称为resolved value)。
  • Rejected(已失败):表示异步操作失败,此时 Promise 会返回一个原因(通常称为rejection reason)。

使用new Promise((resolve, reject) => {...})的方式创建一个Promise对象。在这个构造函数的回调函数中,执行异步操作,并根据操作的结果调用 resolve(成功时)reject(失败时)

js
const myPromise = new Promise((resolve, reject) => {
  // 模拟一个异步操作,比如网络请求
  const randomNumber = Math.random();
  if (randomNumber > 0.5) {
    resolve('Success! The random number is greater than 0.5');
  } else {
    reject('Error! The random number is less than or equal to 0.5');
  }
});
  • then方法用于指定Promise成功时的回调函数,接收resolve返回的值作为参数。
  • catch方法用于指定Promise失败时的回调函数,接收reject返回的原因作为参数。

Promise的一个重要特性是可以通过链式调用的方式将多个异步操作串联起来,形成一个清晰的异步流程。每个.then()方法都可以返回一个新的Promise,从而可以继续在后续的.then()中处理上一个操作的结果。

js
const firstPromise = new Promise((resolve, reject) => {
  resolve(10);
});
const secondPromise = (value) => {
  return new Promise((resolve, reject) => {
    const result = value * 2;
    resolve(result);
  });
};
const thirdPromise = (value) => {
  return new Promise((resolve, reject) => {
    resolve(`Final result: ${value}`);
  });
};
firstPromise
.then(secondPromise)
.then(thirdPromise)
.then((finalResult) => {
    console.log(finalResult);
  });

相比传统的回调方式,Promise可以更好地管理异步操作的流程,避免回调地狱(即多层嵌套的回调函数),使代码更具可读性和可维护性。在复杂的异步操作序列中,使用Promise可以清晰地表达各个操作之间的依赖关系和顺序。

.catch()统一处理错误: 使用.catch()方法可以捕获Promise链中任何一个环节的错误,并进行统一的处理,避免了在每个异步操作中都需要单独处理错误的繁琐代码。

js
const asyncOperation1 = () => {
  return new Promise((resolve, reject) => {
    // 模拟一个可能失败的异步操作
    if (Math.random() > 0.5) {
      resolve('Operation 1 succeeded');
    } else {
      reject('Operation 1 failed');
    }
  });
};
const asyncOperation2 = (value) => {
  return new Promise((resolve, reject) => {
    // 模拟另一个可能失败的异步操作
    if (value.length > 5) {
      resolve('Operation 2 succeeded');
    } else {
      reject('Operation 2 failed');
    }
  });
};
asyncOperation1()
.then(asyncOperation2)
.then((result) => {
    console.log(result);
  })
.catch((error) => {
    console.error('An error occurred:', error);
  });

78、对async/await的理解

async/await实际上是对Promise的一种更高层次的抽象和封装。async/await是JavaScript中处理异步操作的一种方式,它允许我们以类似于同步代码的方式处理异步操作。

async/await是Promise的语法糖。

async关键字用于声明异步函数,该函数返回一个Promise对象。当你在函数前使用async关键字时,可以在这个函数中使用await关键字来暂停执行当前函数,等待一个Promise解决,然后恢复函数的执行并返回解决的值。

js
async function fetchData() {
  let response = await fetch('https://api.example.com/data');
  let data = await response.json();
  return data;
}
fetchData().then(data => console.log(data));

优势

  • 代码可读性增强: 相比传统的基于回调函数或Promise的链式调用的异步代码,async/await使异步代码看起来更像同步代码,更容易理解和维护。它避免了回调地狱(多层嵌套的回调函数)和复杂的Promise链的编写,使代码结构更加清晰。
  • 错误处理更简单: 可以使用传统的try/catch语句来处理异步操作中的错误,与同步代码中的错误处理方式一致,无需在每个Promise的.catch()方法中单独处理错误。
  • 更好的调试体验: 由于代码的执行流程更接近同步代码,在调试异步代码时更容易跟踪和理解代码的执行顺序,方便开发者定位问题。

79、什么是yield表达式

yield关键字通常用于生成器函数(Generator)中,它可以暂停函数的执行并返回一个值,然后再次恢复函数的执行。yield表达式可以看作是生成器函数的一个断点,用于控制函数的执行流程。

yield是一种强大的特性,它使得生成器函数可以创建一个迭代器,并允许我们以更加直观和灵活的方式控制函数的执行流程。

js
function* generator() {
  console.log('step 1');
  yield 'yield value';
  console.log('step 2');
}
const gen = generator();
console.log(gen.next()); // 输出 step 1 和 { value: 'yield value', done: false }
console.log(gen.next()); // 输出 step 2 和 { value: undefined, done: true }

yield表达式可与for...of循环一起使用,以便逐一迭代生成器的返回值(即生成器中使用yield表达式返回的值)。

js
function* generator() {
  yield 'apple';
  yield 'banana';
  yield 'cherry';
}
for (const fruit of generator()) {
  console.log(fruit);
}
// 输出 apple、banana 和 cherry

80、什么是Proxy?可以实现什么功能?

Proxy是ES6引入的一种用于创建对象代理的机制。它允许你在访问对象的属性或执行对象的方法时,拦截并自定义这些操作的行为。

Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

在 Vue3.0 中通过Proxy来替换原本的Object.defineProperty来实现数据响应式。

Proxy是 ES6 中新增的功能,它可以用来自定义对象中的操作。

js
const target = {
  name: 'John',
  age: 30
};
const handler = {
  get: function(obj, prop) {
    console.log(`Accessing property: ${prop}`);
    return obj[prop];
  }
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); 

代表需要添加代理的对象,handler用来自定义对象中的操作,比如可以用来自定义set或者get函数。

81、Reflect对象创建目的?

Reflect是JavaScript中的一个内置对象,它提供了一组用于访问对象属性和执行对象方法的方法。Reflect的设计目的是为了在语言内部提供一种标准的方式来访问和修改对象,以取代一些以前非标准的操作。

Reflect对象的创建目的包括:

  1. 提供标准化的对象操作方法:在JavaScript早期,对象操作的方式多种多样,不统一。Reflect引入后,提供了一组标准的方法,如Reflect.getReflect.setReflect.has等,用于读取、设置属性值,检查属性是否存在等操作,使代码更具一致性和可读性。
  2. 减少全局变量的使用:在以前,一些全局函数和操作符,如deleteinstanceof等,用于操作对象,但它们在一些情况下可能导致不确定的结果或不符合预期的行为。Reflect对象提供了一种更可靠的方式来执行这些操作,减少了对全局变量的依赖。
  3. 提供元编程和代理的支持:Reflect对象是使用JavaScript代理API的基础。代理是元编程的重要工具,用于拦截和自定义对象的操作。Reflect的方法使代理更容易编写,可读性更高。
js
const obj = {
  name: "John",
  age: 30
};
 
// 以前的方式
console.log(obj.name); // 输出 "John"
obj.age = 31;
 
// 使用 Reflect
console.log(Reflect.get(obj, "name")); // 输出 "John"
Reflect.set(obj, "age", 31);

创建代理

js
const target = {
  value: 42
};
 
const handler = {
  get: function(target, prop, receiver) {
    console.log(`获取属性: ${prop}`);
    return Reflect.get(target, prop, receiver);
  }
};
 
const proxy = new Proxy(target, handler);
console.log(proxy.value); // 输出 "获取属性: value",然后输出 42

82、ES6 都有什么Iterator遍历器

Iterator是一种用于遍历集合类数据结构的接口机制。它提供了一种统一的方式来访问数据集合中的元素,而无需关心数据集合的内部存储结构和实现细节。

Iterator的核心思想是通过一个迭代器对象,按照一定的顺序逐个访问数据集合中的元素。这个迭代器对象实现了特定的方法,使得可以在循环或其他迭代操作中方便地获取下一个元素,直到遍历完整个数据集合。

默认部署了Iterator的数据有ArrayMapSetStringTypedArrayargumentsNodeList对象,ES6中有的是SetMap

每个Iterator对象都必须有一个next()方法。当调用next()方法时,它会返回一个对象,这个对象包含两个重要的属性:

  • value:表示当前迭代位置的元素值。如果已经遍历到了数据集合的末尾,value的值为undefined
  • done:一个布尔值,表示迭代是否已经完成。如果迭代完成,即已经遍历到了数据集合的最后一个元素之后,done的值为true,否则为false
js
const myArray = [1, 2, 3];
const myArrayIterator = myArray[Symbol.iterator]();
console.log(myArrayIterator.next()); // { value: 1, done: false }
console.log(myArrayIterator.next()); // { value: 2, done: false }
console.log(myArrayIterator.next()); // { value: 3, done: false }
console.log(myArrayIterator.next()); // { value: undefined, done: true }

83、什么是rest参数?

rest参数,又称剩余参数(形式为...变量名),用于获取函数的多余参数。rest参数之后不能再有其他参数(即只能是最后一个参数)

  1. rest参数是真正意义上的数组,可以使用数组的任何方法
js
function add (x, y, ...rest) {
  console.log(rest) // [3, 4]
  rest.push(123)
  console.log(rest) // [3, 4, 123]
}
add(1, 2, 3, 4)
  1. 函数的length属性,不包含rest
js
function add (x, y, ...rest) {
}
console.log(add.length) // 2
  1. rest参数只能作为最后一个参数,在它之后不能存在任何其他的参数,否则会报错。
js
// 报错:A rest parameter must be last in a parameter list.
function add (x, ...rest, y) {
}

84、什么是尾调用,使用尾调用有什么好处?

尾调用指的是在函数执行的最后一步调用另一个函数。

js
function f(x) {
	return g(x)
}
function b(num) {
  return c(num + 2)
}

下面的两种情况都不属于尾调用:

js
function f(x){
	let y = g(x)
	return y
}
js
function f(x){
	return g(x) + 1
}

为什么说尾调用的性能要比没有使用尾调用的性能好呢?

代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中,这样比较占用内存;
而使用尾调用的话,因为已经是函数的最后一步,执行尾调用函数时,就可以不必再保留当前执行的上下文,从而节省了内存,这就是尾调用优化(尾调用的重要性在于它可以不在调用栈上面添加一个新的堆栈帧,而是更新它,如同迭代一般)。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

尾递归

函数调用自身,叫做递归。如果尾调用自身,则就称为尾递归。

递归非常消耗内存,因为需要同时保存成千上万个调用记录,很容易发生栈溢出的错误。但是对于尾递归来说,由于只存在一个调用记录,所以永远不会发生栈溢出。

  • 求阶乘的递归: 容易造成内存的泄露
    js
    function factorial(n) {
      if (n === 1) return 1
      return n * factorial(n - 1)
    }
    console.log(factorial(5)) //120
    
  • 求阶乘的递归,优化成尾递归
    js
    function factorial(n, total) {
      if (n === 1) return total
      return factorial(n - 1, total * n)
    }
    console.log(factorial(5, 1))
    
    上面代码中使用尾递归,fatorial(5, 1)的值也就是factorial(4, 5)的值,同时也是factorial(3, 20)…这样调用栈中,每一次都只有一个函数,不会导致内存泄露。

ES6中的尾调用优化只在严格模式下开启,正常模式下无效。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。严格模式下禁用这两个变量,所以尾调用模式仅在严格模式下生效。

85、什么是本地存储的有效期?

本地存储的四种方式:cookielocalStorage, sessionStorage, indexDB

  • cookie: 通过expires/max-age设置过期时间。如不指定,则为session cookie, 即一次会话有效。
  • localStorage: 持久存储,需主动清除
  • sessionStorage: 会话存储,会话结束(浏览器,标签页关闭)自动清除。
  • indexDB: 持久存储,需主动删除。

86、toPrecision和toFixed和Math.round的区别?

  • toPrecision 用于处理精度,精度是从左至右第一个不为 0 的数开始数起。
  • toFixed 是对小数点后指定位数取整,从小数点开始数起。
  • Math.round 是将一个数字四舍五入到一个整数。
js
2.134.toPrecision(3) // 2.13
0.012345.toPrecision(3) // 0.0123
0.012345.toFixed(3) // 0.012
Math.round(3.45) // 3
Math.round(3.54) // 4

87、Math.ceil()Math.floor()以及Math.round()的区别?

  • Math.ceil(): 向上取整,函数返回一个大于或等于给定数字的最小整数。
  • Math.floor(): 向下取整,函数返回一个小于或等于给定数字的最大整数。
  • Math.round(): 将一个数字四舍五入到一个整数。
js
Math.ceil(1.35) // 2
Math.floor(1.35) // 1
Math.round(1.5) // 1

88、谈一谈浏览器的缓存机制?

浏览器缓存机制是指浏览器在访问网站时,将一些数据(如HTML、CSS、JS、图片等)缓存在本地,以减少网络传输和提高用户体验。

虽然浏览器缓存可以提高性能和用户体验,但也可能出现缓存过期、缓存劫持等问题,导致用户看到旧的或错误的数据。因此在开发中应该合理使用缓存,并对缓存进行有效的管理和更新,可以使用版本号控制、缓存清除等方式来确保页面和资源的正确性和可用性。

常见的浏览器缓存机制:

  1. 强缓存(HTTP Cache-Control、Expires):当浏览器第一次请求资源时,服务器可以通过设置 HTTP 的 Cache-Control 和 Expires 头部来告诉浏览器该资源是否可以被缓存,以及缓存有效期。在过期时间内,浏览器可以直接从缓存中获取资源,不必再向服务器发送请求。
  2. 协商缓存(HTTP ETag、Last-Modified):当强缓存失效后,浏览器可以通过发送带有 If-Modified-Since 或 If-None-Match 头部的请求,让服务器判断资源是否有更新。如果资源未更改,则服务器返回 304 状态码,告诉浏览器继续使用缓存中的资源,否则返回新的资源。
  3. ServiceWorker缓存:ServiceWorker是运行在浏览器后台的一种JavaScript脚本,可以在离线状态下缓存网页内容、API 数据等,提供更好的离线体验。ServiceWorker可以通过监听fetch事件来拦截网络请求,并根据缓存策略返回结果。

89、Object.assign()

Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

js
var a = {
  name: '张三'
}
var b = {
  age: 23
}
var c = Object.assign(a, b)
console.log(c) // {name: '张三', age: 23}
console.log(a) // {name: '张三', age: 23}
console.log(c) // {age: 23} 

90、URL和URI的区别?

资源:可以通过浏览器访问的信息统称为资源。(图片、文本、HTML、CSS等等。。。)

  • URI是统一资源标识符。标识资源详细名称。包含资源名。
  • URL是统一资源定位符。定位资源的网络位置。包含http:,必须包含协议、主机ip地址。

    URL是一种具体的URI,它是URI的一个子集,它不仅唯一标识资源,而且还提供了定位该资源的信息。

js
http://www.baidu.com ==> URL
/a.html ==> URI
http://www.baidu.com/a.html  ==> 既是URL,又是URI

91、图片的懒加载和预加载

两种技术的本质区别

两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。懒加载适用于长页面、大量图片的情况,而预加载则适用于需要尽快展示的轮播图、广告等场景。

  • 懒加载(Lazy Loading):当网页打开时,只加载可见区域的内容。当用户滚动页面的时候,才会去加载其他未被加载的内容,包括图片、视频等资源。懒加载可以减少网页的加载时间和带宽消耗,提高用户体验。迟缓甚至不加载
    html
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      img {
        width: 100%;
        height: 100%;
      }
      .loading {
        display: none;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 32px;
        height: 32px;
        background-image: url('http://124.222.54.192:3002/picture-ped-img/2023/03/202303311606597.gif');
        background-size: cover;
      }
      .loaded .loading {
        display: none;
      }
    </style>
    <div class="content">
      <div class="image-wrapper" v-for="(item, index) in 10">
        <img :data-src="'./image/' + item + '.png'" alt="" class="lazyload">
        <div class="loading"></div>
      </div>
    </div>
    <script>
      const images = document.querySelectorAll('.lazyload');
      function lazyLoad() {
        images.forEach(image => {
          const rect = image.getBoundingClientRect();
          if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
            const loading = image.nextElementSibling;
            image.onload = () => {
              loading.style.display = 'none';
              image.classList.add('loaded');
            };
            loading.style.display = 'block';
            image.src = image.dataset.src;
          }
        });
      }
      window.addEventListener('scroll', lazyLoad);
      window.addEventListener('load', lazyLoad);
      window.addEventListener('resize', lazyLoad);
    </script>
    
    可以打开F12控制栏看到,只有1、2、3图片被加载,如下图所示,只有滚动时才会去加载剩下的图片 202303311611365.png
  • 预加载(Preloading):在浏览器显示网页之前,提前加载所有或部分页面所需的资源,包括图片、CSS文件、JavaScript文件等。预加载可以加快网页的访问速度,提高用户体验。提前加载
    • 以下是一个简单的示例,在点击按钮时将预加载的图片插入到HTML中,你可以通过F12控制台查看这些图片是否已经预加载完成:
      html
      <button id="btn">添加图片</button>
      <div class="container"></div>
      <script>
        const images = [
          './8.png',
          './9.png',
          './10.png'
        ];
        function preload() {
          images.forEach(src => {
            const img = new Image();
            img.src = src;
          });
        }
        function addImages() {
          const container = document.querySelector('.container');
          images.forEach(src => {
            const img = new Image();
            img.src = src;
            container.appendChild(img);
          });
        }
        window.addEventListener('load', preload);
        document.querySelector('#btn').addEventListener('click', addImages);
      </script>
      
      可以看到我们图片已经被提前加载了,但是我们此时页面上并未去渲染图片,点击按钮会直接从缓存中那图片 202303311614309.png

92、懒加载的概念?懒加载的特点?懒加载的实现原理?

概念

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

特点

  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。
  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
  • 防止加载过多图片而影响其他资源文件的加载:会影响网站应用的正常使用。

实现原理

图片的加载是由src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5的data-xxx属性来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。

懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。

html
<div class="container">
  <img src="loading.gif" data-src="pic1.png" alt="">
  <img src="loading.gif" data-src="pic2.png" alt="">
  <img src="loading.gif" data-src="pic3.png" alt="">
  <img src="loading.gif" data-src="pic4.png" alt="">
  <img src="loading.gif" data-src="pic5.png" alt="">
  <img src="loading.gif" data-src="pic6.png" alt="">
  <img src="loading.gif" data-src="pic7.png" alt="">
</div>
<script>
  var imgs = document.querySelectorAll('img')
  function lazyLoad () {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop
    var winHeight = window.innerHeight
    for (let i = 0; i < imgs.length; i++) {
      if (imgs[i].offsetTop < scrollTop + winHeight) {
        imgs[i].src = imgs[i].getAttribute('data-src')
      }
    }
  }
  window.onscroll = lazyLoad
</script>

93、mouseovermouseenter的区别?

  • mouseover: 当鼠标移动到一个元素上时,会在这个元素上触发 mouseover 事件。对应的移除事件是mouseout
  • mouseenter: 当鼠标移动到元素上时就会触发mouseenter事件。对应的移除事件是mouseleave

mouseovermouseenter主要有以下几个区别:

  1. 触发方式不同:mouseover事件在鼠标指针进入元素或其子元素时触发,而mouseenter事件只在鼠标指针进入元素本身时触发。
  2. 冒泡机制不同:mouseover事件会冒泡到父元素,而mouseenter事件不会冒泡。
  3. 事件对象属性不同:当mouseover事件被触发时,事件对象的fromElement属性表示当前鼠标所在的元素,而mouseenter事件没有类似的属性。
html
<div id="box">
  <div id="box2"></div>
</div>
<script>
  const box = document.getElementById('box');
  const box2 = document.getElementById('box2');
  box.addEventListener('mouseover', (event) => {
    console.log(`Mouseover event: from ${event.fromElement.tagName} to ${event.toElement.tagName}`);
  });
  box.addEventListener('mouseenter', () => {
    console.log('Mouseenter event: enter box');
  });
  box2.addEventListener('mouseover', (event) => {
    console.log(`Mouseover event: from ${event.fromElement.tagName} to ${event.toElement.tagName}`);
  });
  box2.addEventListener('mouseenter', () => {
    console.log('Mouseenter event: enter box2');
  });
</script>

94、JS中文档碎片的理解和使用

什么是文档碎片?

文档碎片可以被视为一个虚拟的DOM节点容器,用于在其中存储多个DOM元素,但这些元素并不会直接在页面中进行渲染显示。

js
document.createDocumentFragment(); // 一个容器,用于暂时存放创建的dom元素

文档碎片有什么用?: 主要用途是在内存中构建一组DOM元素,然后一次性将这些元素添加到文档中,从而减少DOM操作的重绘和重排,提高性能。

将需要添加的大量元素,先添加到文档碎片中,再将文档碎片添加到需要插入的位置,大大减少dom操作,提高性能(IE和火狐比较明显)。

示例:往页面上放100个元素。

js
// 普通方式:(操作了100次dom)
// 通过for循环,每次循环,添加一个dom元素
for (var i = 100; i > 0; i--) {
  var elem = document.createElement("div");
  document.body.appendChild(elem); // 放到body中
}

// 文档碎片:(操作1次dom)
// 先将dom暂存在文档碎片中,然后在一次性操作dom
var df = document.createDocumentFragment();
for (var i = 100; i > 0; i--) {
  var elem = document.createElement("div");
  df.appendChild(elem);
}
// 最后放入到页面上
document.body.appendChild(df);

95、offsetWidth/offsetHeight, clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别?

client系列

  • clientWidth/clientHeight返回的是元素的内部宽度,它的值只包含content + padding,如果有滚动条,不包含滚动条。
  • clientTop返回的是上边框的宽度。
  • clientLeft返回的左边框的宽度。

offset系列

  • offsetWidth/offsetHeight返回的是元素的布局宽度,它的值包含content + padding + border包含了滚动条。
  • offsetTop返回的是当前元素相对于其offsetParent元素的顶部的距离。
  • offsetLeft返回的是当前元素相对于其offsetParent元素的左部的距离。

scroll系列

  • scrollWidth/scrollHeight返回值包含content + padding + 溢出内容的尺寸
  • scrollTop属性返回的是一个元素的内容垂直滚动的像素数。
  • scrollLeft属性返回的是元素滚动条到元素左边的距离。

96、layerX/layerY、offsetX/offsetY、pageX/pageY、clientX/clientY、screenX/screenY、x/y区别

  • layerX/layerY

    如果触发元素没有设置定位,则以页面左上角为参考点;如果触发元素有设置定位,则以触发元素左上角为参考点;

  • offsetX/offsetY

    鼠标相对于触发事件的元素位置内容区左上角的坐标;

  • pageX/pageY

    鼠标相对于文档左上角的坐标,pageY = clientY + scrollY

  • clientX/clientY

    鼠标相对于当前浏览器窗口左上角的坐标;

  • screenX/screenY

    鼠标相对于用户显示器屏幕左上角的坐标;

  • x/y

    和clientX、clientY一样;

html
<style>
  .father {
    width: 200px;
    height: 200px;
    background: #ccc;
    display: flex;
    justify-content: center;
    align-items: center;
    position: relative;
  }
  .child {
    width: 50px;
    height: 50px;
    background: blue;
    color: #fff;
  }
</style>
<div class="father">
  <div class="child">child</div>
</div>
<script>
  document.querySelector('.child').addEventListener('click', (event) => {
    console.log(event)
  })
</script>

点击child盒子的任何地方,然后看控制台输出结果 2023032111290710.png 从上面结果看到:

js
layerX: 87
layerY: 92
clientX: 98 = layerX + 8px(浏览器默认间距)
clientY: 100 = layerY + 8px(浏览器默认间距)
offsetX: 12
offsetY: 17
pageX: 95
pageY: 100

97、web开发中会话跟踪的方法有哪些

会话跟踪: 从用户进入一个网站浏览到退出这个网站或者关闭浏览器称为一次会话。会话跟踪是指在这个过程中浏览器与服务器的多次请求保持数据共享的状态的技术。

1、cookie

Cookie是客户端技术,程序把每个用户的数据以cookie的形式写给用户各自的浏览器。当用户使用浏览器再去访问服务器中的web资源时,就会带着各自的数据去。

2、session

Session 是存储在服务端的,并针对每个客户端(客户),通过SessionID来区别不同用户的。 该会话过程直到session失效(服务端关闭),或者客户端关闭时结束。相比cookie更安全,一般网站是session结合着cookie一起使用的。

3、url重写

客户程序在每个URL的尾部添加一些额外数据。这些数据标识当前的会话,服务器将这个标识符与它存储的用户相关数据关联起来。 URL重写是比较不错的会话跟踪解决方案,即使浏览器不支持 cookie 或在用户禁用 cookie 的情况下,这种方案也能够工作。 最大的缺点是每个页面都是动态的,如果用户离开了会话并通过书签或链接再次回来,会话的信息也会丢失,因为存储下来的链接含有错误的标识信息。

4、隐藏input

提交表单时,要将指定的名称和值自动包括在 GET 或 POST 数据中。这个隐藏域可以用来存储有关会话的信息。

html
<input type="hidden" name="content" value="haha">

主要缺点是:仅当每个页面都是由表单提交而动态生成时,才能使用这种方法。

98、eval是做什么的?

eval功能是把对应的字符串解析成JS代码并运行;但不安全,非常耗性能。原因在于:该过程会执行两次:

  • 一次解析成 js 语句
  • 一次执行 js 语句

99、attribute和property的区别是什么?

中文翻译property:属性,attribute:特性

  • property是DOM中的属性,是JavaScript里的对象;

    property是这个DOM元素作为对象,其附加的内容,例如childNodes、firstChild等。

  • attribute是HTML标签上的特性,它的值只能够是字符串;

    attribute就是dom节点自带的属性,例如html中常用的id、class、title、align等。

100、谈一谈你理解的函数式编程?

主要的编程范式有三种:命令式编程、声明式编程、函数式编程。

  • 函数式编程思想是把运算过程抽象成一个函数,定义好输入参数,只关心它的输出结果。
  • 命令式编程是一种告诉计算机如何一步步执行任务的编程范式,它关注程序的执行步骤和流程控制。
  • 声明式编程则是一种告诉计算机要做什么,但不指定具体如何做的编程范式。它关注程序的逻辑和数据结构,而不是程序的执行流程。

函数式编程能够更大程度上复用代码,减少冗余,vue3.0重构运用了大量的函数式编程,react也是如此。

101、在js中哪些会被隐式转换为false(或假值对象有哪些)

UndefinednullfalseNaN0空字符串

102、列举浏览器对象模型BOM里常用对象,并列举window对象的常用方法至少5个?

  • 对象:WindowLocationScreenHistoryNavigator
    • window: 是 JS 的最顶层对象,其他的 BOM 对象都是window对象的属性;
    • location: 浏览器当前 URL 信息;
    • navigator: 浏览器本身信息;
    • screen: 客户端屏幕信息;
    • history: 浏览器访问历史信息;
    • 存储对象: localStorage、sessionStorage、
  • 方法:alert()confirm()prompt()open()close()

    参考:https://www.runoob.com/jsref/obj-window.html

DOM对象

  • Document: 提供了访问和更新HTML页面内容的属性和方法。
  • Node: 提供了用于解析DOM节点树结构的属性和方法。
  • Element: 继承于Node的。

103、下面两种setInterval的调用方式有什么区别?

js
function fn1 () {
  console.log(1)
}
function fn2 () {
  console.log(2)
}
setInterval(fn1, 500) // 每隔 500ms 执行一次
setInterval(fn2(), 500) // 只执行一次,并且是立即执行

第一个是重复执行每 500 毫秒执行一次,后面一个只执行一次。

104、自动分号

有时 JavaScript 会自动为代码行补上缺失的分号,即自动分号插入(Automatic SemicolonInsertion,ASI)。
因为如果缺失了必要的;,代码将无法运行。如果 JavaScript 解析器发现代码行可能因为缺失分号而导致错误,那么它就会自动补上分号。并且,只有在代码行末尾与换行符之间除了空格和注释之外没有别的内容时,它才会这样做。

js
var a = 1
(() => {
  console.log(123)
})()

会报错: Uncaught TypeError: 1 is not a function

105、ES6引入Symbol的原因

ES5的对象属性名都是字符串,这容易造成属性名的冲突。

比如: 你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

106、documen.write和innerHTML的区别?

  1. document.write是一个方法,是重写整个document, 写入内容是字符串的html
html
<button id="btn">点击</button>
<script>
  const btn = document.querySelector('#btn')
  btn.addEventListener('click', () => {
    document.write('<div style="color: red;">哈哈哈哈</div>')
  })
</script>
  1. innerHTML是一个属性,是HTMLElement的属性,是一个元素的内部html内容。

107、window.location的注意事项?

window.location.search

查询(参数)部分,即url上问号?后面的内容

js
// 比如当前url是 http://www.baidu.com?ver=1.0&id=timlq
window.location.search // ?ver=1.0&id=timlq

window.location.hash

锚点,返回值:#love

js
// 比如当前url是 http://dev.app.puliedu.com/#/backstage/sampleLabs
window.location.hash // #/backstage/sampleLabs

window.location.reload()

它主要是用来刷新当前页面。

108、为什么扩展javascript内置对象不是好的做法?

因为扩展内置对象会影响整个程序中所使用到的该内置对象的原型属性。

109、如何测试前端代码? 知道怎么测试你的前端工程么?

TDD

TDD英文全称为:Test Driven Development表示测试驱动开发,它是一种测试驱动开发,它是一种测试先于编写代码的思想用于指导软件开发。简单地说就是先根据需求写测试用例,再代码实现,接着测试,循环此过程直到产品的实现。 特点:

  • 有利于更加专注软件设计
  • 清晰地了解软件的需求
  • 很好的诠释了代码即文档

BDD

BDD英文全称为:Behavior Driven Development表示行为驱动开发,它鼓励软件开发者,测试人员和非技术人员或者商业参与者之间的协作。BDD可以看作是对TDD的一种补充,或者说是TDD的一个分支。BDD更加依赖于需求行为和文档来驱动开发,这些文档的描述跟测试代码很相似。e2e测试更多是和BDD的开发模式进行结合。

unit test

unit test为单元测试,主要用于测试开发人员编写的代码是否正确,这部分工作都是由开发人员自己来做的。

mocha Mocha.js是一个流行的 JavaScript 测试框架,它提供了一组简单易用的 API 用于编写和运行测试。下面是一个简单的 Mocha.js 测试用例示例:

js
const assert = require('assert');

describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal(-1, [1,2,3].indexOf(4));
    });
  });
});

该示例定义了描述测试套件的describe函数,其中包含一个或多个描述测试用例的it函数。在该示例中,我们编写了一个测试用例来测试数组中的元素是否存在并返回其索引值。使用assert模块进行断言。

要运行此测试用例,你需要先安装 Mocha.js:

sh
npm install --global mocha

然后,在命令行中进入测试文件所在目录,运行以下命令:

sh
mocha test.js

其中test.js是包含测试用例的文件名。执行完毕后,将显示测试结果。

110、如何添加html元素的事件,有几种方法?

方法一:在HTML元素当中绑定事件

html
<div class="wrap" onclick="show()">绑定事件一</div>
<script type="text/javascript">
  function show() {
    alert('绑定事件一');
  }
</script>

方法二:使用js给元素绑定事件

html
<div class="wrap" id="btn">绑定事件二</div>
<script type="text/javascript">
  var btnEle = document.querySelector('#btn');
  btnEle.onclick = show;
  function show() {
    alert('绑定事件二');
  }
</script>

方法三:使用事件注册函数

html
<div class="wrap" id="btn">绑定事件三</div>
<script type="text/javascript">
  var btnEle = document.querySelector('#btn');
  btnEle.addEventListener('click', show, !1);
  function show() {
    alert('绑定事件三');
  }
</script>

111、如何自定义事件?

自定义事件有如下三种方式:

  • 1、createEventinitEventdispatchEvent三件套
    • createEvent设置事件类型,是 html 事件还是 鼠标事件
    • initEvent初始化事件,事件名称,是否允许冒泡,是否阻止自定义事件
    • dispatchEvent触发事件
    html
    <div style="width:100px;height:100px;background-color: bisque;" id="div01"></div>
    <script>
      // 1、创建事件.
      var event = document.createEvent('Event');
      const el = document.getElementById("div01")
      // 2、初始化一个点击事件,可以冒泡,无法被取消
      event.initEvent('dianJi', true, false);
      // 3、设置事件监听.
      el.addEventListener('dianJi', function(e) {
        console.log('打印了', e)
      }, false);
      // 4、触发事件监听
      el.dispatchEvent(event);
    </script>
    
  • 2、Event()
    html
    <div style="width:100px;height:100px;background-color: bisque;" id="div01"></div>
    <script>
      // 1、创建事件.
      const event = new Event('custom');
      const el = document.getElementById("div01")
      // 2、设置事件监听.
      el.addEventListener('custom', (e) => {
        console.log('打印', e)
      });
      // 3、触发事件监听
      // 必须使用Dom元素将该事件分发出去,否则无法进行监听
      el.dispatchEvent(event);
    </script>
    
  • 3、CustomEvent()
    html
    <div style="width:100px;height:100px;background-color: bisque;" id="div01"></div>
    <script>
      // 1、创建事件.
      const event = new CustomEvent('custom', { detail: { language: 'JavaScript' } });
      const el = document.getElementById("div01")
      // 2、设置事件监听.
      el.addEventListener('custom', (e) => {
        console.log('打印', e.detail) //  { language: 'JavaScript' }
      });
      // 3、触发事件监听
      // 必须使用Dom元素将该事件分发出去,否则无法进行监听
      el.dispatchEvent(event);
    </script>
    

112、如何避免重绘或者重排?

  • 减少直接操作dom元素,改用className用于控制或者使用cssText统一设置样式

    可以添加一个类,样式都集中在类中改变

    js
    // 使用cssText
    const el = document.getElementById('test');
    el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
    
    // 修改CSS的class
    const el = document.getElementById('test');
    el.className += ' active'; 
    
  • 尽量减少table使用,table属性变化使用会直接导致布局重排或者重绘
  • dom元素position属性为fixed或者absolute,脱离文档流,可以通过css形变触发动画效果,此时是不会出发reflow
  • 不要把DOM结点的属性值放在一个循环里当成循环里的变量
  • 如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document
  • 能用css3实现的就用css3实现

    比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!

    • 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘
    • 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

113、delete数组的item,数组的length是否会-1

delete Array[index]

js
const arr = ['a', 'b', 'c', 'd', 'e'];
let result = delete arr[1];
console.log(result); // true;
console.log(arr); // ['a', undefined, 'c', 'd', 'e']
console.log(arr.length); // 5
console.log(arr[1]); // undefined

使用delete删除元素,返回truefalse, true表示删除成功,false表示删除失败。使用delete删除数组元素并不会改变原数组的长度,只是把被删除元素的值变为undefined

114、给出['1', '3', '10', '4', 112].map(parseInt)执行结果

结果是: [1, NaN, 2, NaN, 22]

map使用语法

js
[1, 2, 3].map(function(current, index, arr) {
})

参数解析:

  • current: 当前元素值
  • index: 当前元素索引值
  • arr: 数组本身

parseInt使用语法

js
parseInt(str, radix)

参数解析:

  • str: 需要解析的字符串
  • radix: ⼏进制
    • 若省略或为0,则以10进⾏解析
    • 若⼩于2或者⼤于36,则返回NaN

所以该题展开来写:

js
const result = ['1', '3', '10'].map(function(cur, index, arr) {
return parseInt(cur, index);
});
// 执⾏过程:
// parseInt('1', 0) -> 1
// parseInt('3', 1) -> 由于基数为1,不在2~36之间,则返回NaN
// parseInt('10', 2) -> // 基数为2,故结果为 1 * 2 ^ 1 + 0 * 2 ^ 0 = 2
// parseInt('4', 3) -> 由于基数为3,而三进制的取值范围是0、1、2,此处为4,不在三进制取值范围,故NaN
// parseInt('112', 4) -> 由于基数为4,故结果为 1 * 4 ^ 2 + 1 * 4 ^ 1 + 2 * 4 ^ 0 = 22

115、什么是工厂函数

JavaScript 工厂函数(Factory Function)是一种创建和返回对象的函数,它不需要使用new关键字来将一个类构造函数实例化,而是直接返回一个新的对象,通常用于抽象对象的创建过程。

工厂函数可以接受任意数量的参数,并使用这些参数来生成新的对象。由于工厂函数本身就是一个普通的 JavaScript 函数,因此它具有一些常规的函数特点,比如可以使用函数作用域、闭包等技术来隐藏实现细节、保护私有数据等。

js
function createStudent(name, age, gender) {
  return {
    name: name,
    age: age,
    gender: gender,
    study: function(subject) {
      console.log(`${this.name} is studying ${subject}.`);
    }
  };
}

116、数组降维

  • 1、数组字符串化
    js
    let arr = [
      [222, 333, 444],
      [55, 66, 77], {
        a: 1
      }
    ]
    arr += '';
    arr = arr.split(',');
    console.log(arr); // ["222", "333", "444", "55", "66", "77", "[object Object]"]
    
    这也是比较简单的一种方式,从以上例子中也能看到问题,所有的元素会转换为字符串,且元素为对象类型会被转换为 "[object Object]" ,对于同一种类型数字或字符串还是可以的。
  • 2、利用applyconcat转换
    js
    function reduceDimension(arr) {
      return Array.prototype.concat.apply([], arr);
    }
    
    console.log(reduceDimension([
      [123], 4, [7, 8],
      [9, [111]]
    ])); // [123, 4, 7, 8, 9, Array(1)]
    
  • 3、递归
    js
    function reduceDimension(arr) {
      let ret = [];
      let toArr = function(arr) {
        arr.forEach(function(item) {
          item instanceof Array ? toArr(item) : ret.push(item);
        });
      }
      toArr(arr);
      return ret;
    }
    
  • 4、Array​.prototype​.flat()
    js
    var arr1 = [1, 2, [3, 4]];
    arr1.flat();
    // [1, 2, 3, 4]
    
    var arr2 = [1, 2, [3, 4, [5, 6]]];
    arr2.flat();
    // [1, 2, 3, 4, [5, 6]]
    
    var arr3 = [1, 2, [3, 4, [5, 6]]];
    arr3.flat(2);
    // [1, 2, 3, 4, 5, 6]
    
    //使用 Infinity 作为深度,展开任意深度的嵌套数组
    arr3.flat(Infinity);
    // [1, 2, 3, 4, 5, 6]
    
  • 5、使用reduceconcat和递归无限反嵌套多层嵌套的数组
    js
    var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
    function flattenDeep(arr1) {
      return arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
    }
    flattenDeep(arr1);
    // [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
    

117、JavaScript中undefinednot defined以及undeclared的区别

  • undefined是没有初始化,表示变量已申明,未赋值;
  • not defined是没有声明;
  • undeclared表示未声明,未赋值;

118、在JavaScript中创建一个真正的private方法有什么缺点?

每一个对象都会创建一个private方法的方法,这样很耗费内存。

js
var Employee = function(salary) {
  this.salary = salary;
  // 私有方法
  var increaseSalary = function() {
    this.salary = this.salary + 100;
  };
  // 公共方法
  this.printIncreaseSalary = function() {
    increaseSlary();
    console.log(this.salary);
  };
};

// Create Employee class object
var emp1 = new Employee(3000);
// Create Employee class object
var emp2 = new Employee(2000);

在这里emp1, emp2都有一个increaseSalary私有方法的副本。所以我们除非必要,非常不推荐使用私有方法。

119、JavaScript怎么清空数组?

方法1

js
arrayList = [];

直接改变arrayList所指向的对象,原对象并不改变。

方法2

js
arrayList.length = 0;

这种方法通过设置length = 0使原数组清除元素。

方法3

js
arrayList.splice(0, arrayList.length);

方法四: 该方案不是很简洁,也是最慢的解决方案,与原始答案中引用的早期基准相反。

js
while (arrayList.length) {
  arrayList.pop()
}

120、javascript的同源策略

所谓同源是指,协议,域名,端口相同,同源策略是一种安全协议。不同源的客户端脚本(javascript、ActionScript)在没明确授权的情况下,不能读写对方的资源。

简单的来说,浏览器允许包含在页面A的脚本访问第二个页面B的数据资源,这一切是建立在A和B页面是同源的基础上。

121、什么是跨域?跨域请求资源的方法有哪些?

跨域问题是指当一个Web页面向不同源的服务器请求资源时(例如向其他域名、端口或协议发送XMLHttpRequest请求),浏览器会阻止这种行为,因为它可能会引起安全漏洞。

常用的跨域解决办法

  1. proxy代理:proxy代理用于将请求发送给后台服务器,通过服务器来发送请求,然后将请求的结果传递给前端。例如:nginx代理、代理后台等;
  2. CORS:跨域资源共享,Cross-Origin Resource Sharing;
js
res.writeHead(200, {
  "Content-Type": "text/html; charset=UTF-8",
  "Access-Control-Allow-Origin":'http://localhost',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'X-Requested-With, Content-Type'
});
  1. jsonp:JSONP利用script标签没有跨域限制的特性,通过callback函数来实现跨域数据的传输;
html
<script>
  function testjsonp(data) {
    console.log(data.name); // 获取返回的结果
  }
</script>
<script>
  var _script = document.createElement('script');
  _script.type = "text/javascript";
  _script.src = "http://localhost:8888/jsonp?callback=testjsonp";
  document.head.appendChild(_script);
</script>
  1. Websocket:WebSocket是一种基于TCP协议的新型网络传输协议,支持双向通信。由于WebSocket协议定义了与HTTP不同的握手方式,因此也不存在同源限制。

122、用正则把yya yyb yyc变成yya5 yyb6 yyc7?

js
j = 5;
str.replace(/\w+/g, function(m) {
  return m + j++;
});
// function的第一参数代表匹配正则的字符串,第二个代表第一个子表达式匹配的字符串,第三个代表第二个子表达式匹配的字符串。

123、怎么判断两个json对象的内容相等?

1、通过JSON.stringify()方法转成字符串对比

js
obj = { a: 1, b: 2 }
obj2 = { a: 1, b: 2 }
obj3 = { a: 1, b: 2 }
JSON.stringify(obj) == JSON.stringify(obj2); //true
JSON.stringify(obj) == JSON.stringify(obj3); //false

2、通过循环实现

js
const obj = { a: 1, b: 2 }
const obj2 = { a: 1, b: 3 }
let flag = true
for (let key in obj) {
  if (obj[key] !== obj2[key]) {
    flag = false
  }
}

124、关于函数的length属性

js
((a, b) => a + b).length === 2; // 输出true

函数是有length属性的,函数的length属性就是函数参数的个数,函数的参数就是arguments,而arguments也是一个类数组对象所以他是有length属性的

125、数组中字符串键值的处理

在 JavaScript 中数组是通过数字进行索引,但是有趣的是他们也是对象,所以也可以包含 字符串 键值和属性,但是这些不会被计算在数组的长度(length)内,如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当做数字索引来处理

js
const arr = [];
arr[0] = 1;
arr['1'] = '嘿嘿';
arr['cym'] = 'cym';
console.log(arr); // [1, '嘿嘿', cym: 'cym']
console.log(arr.length); // 2

126、什么是链表?

链表是一种物理存储单元上非连续、非顺序的存储结构。链表由一系列结点组成,结点可以在运行时动态生成。

每个结点包括两个部分:

  • 一个是存储数据元素的数据域;
  • 另一个是存储下一个结点地址的指针域。

js模拟链表:

  • 模拟定义
    js
    const a = { val: 'a' }
    const b = { val: 'b' }
    const c = { val: 'c' }
    const d = { val: 'd' }
    a.next = b
    b.next = c
    c.next = d
    console.log(a)
    
    202303041736453.png
  • 遍历链表
    js
    let p = a // 声明一个指针, 指向链表的头部 a
    while (p) {
      console.log(p.val)
      p = p.next // 依次指向下一个链表元素
    }
    
  • 插入链表
    js
    const e = { val:  'e' }
    d.next = e
    
  • 删除
    js
    c.next = e
    

127、什么是堆?什么是栈?它们之间有什么区别和联系?

堆和栈都是计算机内存中的一种数据结构,它们之间有以下区别和联系:

  • 定义:
    • 栈是一种后进先出(LIFO)的数据结构,用于存储函数调用、局部变量等;
    • 堆是一种动态分配的数据结构,用于存储程序运行时动态生成的对象。
  • 存储方式:
    • 栈采用顺序存储结构,所有元素在同一块连续的内存空间中;
    • 堆采用链式存储结构,元素可以分布在不同的内存区域,并通过指针相互连接。
  • 内存管理:
    • 栈的内存分配和回收由系统自动完成,无需手动干预;
    • 堆的内存分配和回收需要手动进行操作,否则会导致内存泄漏或内存溢出等问题。
  • 访问速度:由于栈采用顺序存储结构,其访问速度比堆更快;而堆采用链式存储结构,访问速度较慢。
  • 适用场景:栈适用于多层函数调用、递归等场景,堆适用于存储动态数据结构,如对象、数组等。
  • 联系:栈和堆都是内存中的数据结构,程序在运行时使用栈和堆来存储数据。在某些情况下,栈和堆可能同时被使用,例如函数中创建一个对象时,对象的引用存储在栈上,而对象本身存储在堆中。

128、栈、堆、队列和哈希表的区别?

栈(Stack)、堆(Heap)、队列(Queue)和哈希表(Hash Table)是常见的数据结构,它们之间的主要区别如下:

  • 栈(Stack):栈是一种后进先出(LIFO)的数据结构。只允许在栈顶进行插入和删除操作,类似于弹夹式装弹机。常用于处理递归函数、表达式求值、内存分配等场景。
  • 堆(Heap):是一种树形数据结构,在堆中,每个节点都有一个值,通常所说的堆指的是二叉堆,满足一定的堆特性。堆是一个完全二叉树,且父节点的值大于或小于它的左右子节点的值,被称为大根堆或小根堆。常用于实现优先队列、动态存储等场景。
  • 队列(Queue):队列是一种先进先出(FIFO)的数据结构。只允许在队尾进行插入操作,在队头进行删除操作,类似于排队买票。常用于实现消息队列、缓存队列等场景。
  • 哈希表(Hash Table):是一种通过散列函数将键映射到值的数据结构。哈希表通常具有快速的查找、插入和删除操作,并且可以通过合理的设置散列函数来减少冲突。常用于实现字典、缓存等场景。

129、js获取原型的方法?

基础代码

js
function Person () {
}
const p = new Person()
  • p.__proto__
    js
    console.log(p.__proto__) // {constructor: ƒ Person ()}
    
  • p.constructor.prototype
    js
    console.log(p.constructor.prototype) // {constructor: ƒ Person ()}
    // 等价于
    console.log(Person.prototype)
    
  • Object.getPrototypeOf(p)
    js
    console.log(Object.getPrototypeOf(p)) // {constructor: ƒ Person ()}
    

130、在js中不同进制数字的表示方式

  • 0X、0x 开头的表示为十六进制。
  • 0、0O、0o 开头的表示为八进制。
  • 0B、0b 开头的表示为二进制格式。
  • 十进制就不用特殊表示,因为默认为十进制。

131、js中整数的安全范围是多少

安全整数指的是,在这个范围内的整数转化为二进制存储的时候不会出现精度丢失,能够被“安全”呈现的最大整数。

  • 最大整数2^53 - 1,即9007199254740991,在 ES6 中被定义为Number.MAX_SAFE_INTEGER
  • 最小整数-2^53 + 1,是-9007199254740991,在 ES6 中被定义为 Number.MIN_SAFE_INTEGER
  • 如果某次计算的结果得到了一个超过 JavaScript 数值范围的值,那么这个值会被自动转换为特殊的Infinity值。如果某次计算返回了正或负的Infinity值,那么该值将无法参与下一次的计算。
  • 判断一个数是不是有穷的,可以使用isFinite函数来判断。

132、Array构造函数只有一个参数值时的表现?

Array构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。这样创建出来的只是一个空数组,只不过它的 length 属性被设置成了指定的值。

js
// 常规方式
var arr1 = new Array(3) // new Array()
arr1[0] = 'a'
arr1[1] = 'b'
arr1[2] = 'c'

// 简洁方式
var arr2 = new Array('a', 'b', 'c') // ['a', 'b', 'c']
var arr3 = new Array('aa') // ['aa']
var arr3 = new Array(false) // [false]

// 字面方式
var arr4 = ['a', 'b', 'c']

构造函数Array(..)不要求必须带new关键字。不带时,它会被自动补上。

js
Array(4) // [empty x 4]
new Array(4) // [empty x 4]

133、~操作符的作用?

~返回2的补码,并且~会将数字转换为32位整数,因此我们可以使用~来进行取整操作。
~x大致等同于-(x+1)

js
~2 // 等同于 -(2+1) = -3

134、Symbol 值的强制类型转换?

  • Symbol值转化为字符换,存在显示转换,和隐式转换。
    • 显示转化就直接转换为对应的字符串,但是隐式转换就会报错。
  • Symbol值转换为数字类型的值,无论是隐式转换还是显示转换都会报错。
  • Symbol值转化为布尔类型的值,无论显示转换还是隐式转换都返回true

135、==操作符的强制类型转换规则?

  1. 如果两个操作数的类型相同,则直接比较它们的值。
  2. 如果其中一个操作数是null,另一个操作数必须是undefined或者null才会返回true,否则返回false
  3. 如果其中一个操作数是数字,另一个操作数是字符串,则将字符串转换为数字。
  4. 如果其中一个操作数是布尔值,另一个操作数是非布尔值,则将布尔值转换成数字01,再跟另一个操作数进行比较。
  5. 如果其中一个操作数是对象,另一个操作数是字符串或数字,则将对象转换为原始类型的值(即调用valueOftoString方法),再跟另一个操作数进行比较。如果对象不能被转换成原始类型的值,则返回false
  6. 如果其中一个操作数是NaN,则返回false。注意,NaN不等于任何值,包括它本身。
  7. 如果两个操作数都是对象,则比较它们的引用是否相等。即使两个对象具有相同的属性和值,但它们在内存中的位置不同,也会返回false

136、如何将字符串转化为数字,例如 '12.3b'?

  • (1)使用 Number() 方法,前提是所包含的字符串不包含不合法字符。
    js
    Number('123.b') // NaN
    Number('123') // 123
    Number('123.1') // 123.1
    
  • (2)使用 parseInt() 方法,parseInt() 函数可解析一个字符串,并返回一个整数。还可以设置要解析的数字的基数。当基数的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。
    js
    parseInt('123.b') // 123
    parseInt('b123') // NaN
    
  • (3)使用 parseFloat() 方法,该函数解析一个字符串参数并返回一个浮点数。
    js
    parseFloat('123.b') // 123
    parseFloat('123.1b') // 123.1
    parseFloat('b123') // NaN
    
  • (4)使用 + 操作符的隐式转换,前提是所包含的字符串不包含不合法字符。

137、如何将浮点数点左边的数每三位添加一个逗号,如 12000000.11 转化为『12, 000, 000.11』?

js
// 方法一
function format(number) {
  return number && (number + '').replace(/(?!^)(?=(\d{3})+\.)/g, ",");
}
// 方法二
function format1(number) {
  return Intl.NumberFormat().format(number)
}
// 方法三
function format2(number) {
  return number.toLocaleString('en')
}

138、什么是 DOM 和 BOM?

DOM

DOM 全称是Document Object Model,也就是文档对象模型。是针对XML的基于树的API。描述了处理网页内容的方法和接口,是HTML和XML的API,DOM把整个页面规划成由节点层级构成的文档。

BOM

BOM是Browser Object Model,浏览器对象模型。刚才说过DOM是为了操作文档出现的接口,那BOM顾名思义其实就是为了控制浏览器的行为而出现的接口。

139、什么是Polyfill?

Polyfill指的是用于实现浏览器并不支持的原生API的代码。比如说querySelectorAll是很多现代浏览器都支持的原生Web API,但是有些古老的浏览器并不支持,那么假设有人写了一段代码来实现这个功能使这些浏览器也支持了这个功能,那么这就可以成为一个Polyfill

简言之,polyfill是用旧语法重写新版本新增的方法(api),以兼容旧版浏览器。

polyfill是一个js脚本,我们可以只针对一个方法引入,比如promise引入相应的polyfill,也可以引入一整个文件,一般来说我们会使用现成的npm包,有很多包供我们选择

sh
npm i promise-polyfill

常用的还有

sh
npm i babel-polyfill

还有一个babel,是我们常见的做低版本兼容的工具包,babelpolyfill的区别在于:

  • babel只转化新的语法,不负责实现新版本js中新增的api
  • polyfill负责实现新版本js中新增的api 例如使用polyfill实现Object.is
    js
    if (!Object.is) {
      Object.defineProperty(Object, "is", {
        value: function (x, y) {
          // SameValue algorithm
          if (x === y) {
            // return true if x and y are not 0, OR
            // if x and y are both 0 of the same sign.
            // This checks for cases 1 and 2 above.
            return x !== 0 || 1 / x === 1 / y;
          } else {
            // return true if both x AND y evaluate to NaN.
            // The only possibility for a variable to not be strictly equal to itself
            // is when that variable evaluates to NaN (example: Number.NaN, 0/0, NaN).
            // This checks for case 3.
            return x !== x && y !== y;
          }
        }
      });
    }
    
  • 所以在兼容的时候一般是babel + polyfill都用到,所以babel-polyfill一步到位

140、移动端最小触控区域是多大?

移动端最小触控区域44*44px,再小就容易点击不到或者误点。

参考:移动端最小触控区域44*44px

141、移动端的点击事件的有延迟,时间是多久,为什么会有? 怎么解决这个延时?

  1. 300 毫秒
  2. 因为浏览器捕获第一次单击后,会先等待一段时间,如果在这段时间区间里用户未进行下一次点击,则浏览器会做单击事件的处理。如果这段时间里用户进行了第二次单击操作,则浏览器会做双击事件处理。
  3. 推荐fastclick.js

142、使用构造函数的注意点

  1. 一般情况下构造函数的首字母需要大写,因为我们在看到一个函数首字母大写的情况,就认定这是一个构造函数,需要跟new关键字进行搭配使用,创建一个新的实例(对象)
  2. 构造函数在被调用的时候需要跟new关键字搭配使用。
  3. 在构造函数内部通过this+属性名的形式为实例添加一些属性和方法。
  4. 构造函数一般不需要返回值,如果有返回值
  • 如果返回值是一个基本数据类型,那么调用构造函数,返回值仍旧是那么创建出来的对象。
js
function Person (name) {
  this.name = name
  return 'aaa'
}
const person = new Person('张三')
console.log(person) // Person {name: '张三'}
  • 如果返回值是一个复杂数据类型,那么调用构造函数的时候,返回值就是这个return之后的那个复杂数据类型
js
function Person (name) {
  this.name = name
  return {
    demo: '123'
  }
}
const person = new Person('张三')
console.log(person) // {demo: '123'}

143、如何实现文件断点续传?

在JavaScript中实现文件断点续传主要涉及到前端和后端的配合,前端负责分片文件的处理,而后端则需要支持接收分片并重组文件的逻辑。

前端

  1. 文件分片:使用File API将大文件切割成多个小片段。
  2. 发送文件片段:使用XMLHttpRequest或Fetch API逐个上传文件片段。
  3. 记录上传进度:在上传每个片段时,记录已上传的片段和进度。
  4. 处理上传错误:如果上传过程中出现错误,尝试重新上传失败的片段。
js
function uploadFile(file, start, end) {  
  const chunk = file.slice(start, end);  
  const formData = new FormData();  
  formData.append('file', chunk);  
  formData.append('start', start);  
  formData.append('end', end);  
    
  fetch('/upload', {  
    method: 'POST',  
    body: formData  
  })  
  .then(response => {  
    if (!response.ok) {  
      throw new Error('Upload failed');  
    }  
    console.log(`Chunk ${start}-${end} uploaded`);  
  })  
  .catch(error => {  
    console.error('Error uploading chunk:', error);  
    // Retry logic here  
  });  
}  
  
function splitFileAndUpload(file) {  
  const chunkSize = 1024 * 1024; // 1MB  
  let start = 0;  
  let end = chunkSize;  
  while (start < file.size) {  
    if (end > file.size) {  
      end = file.size;  
    }  
    uploadFile(file, start, end);  
    start = end;  
    end += chunkSize;  
  }  
}  
// 使用示例  
const file = document.querySelector('input[type="file"]').files[0];  
splitFileAndUpload(file);

后端

  1. 接收文件片段:在后端设置路由来接收文件片段。
  2. 保存文件片段:将接收到的每个片段保存到服务器上的临时位置。
  3. 重组文件:当所有片段都上传完毕后,将它们组合成一个完整的文件。
js
const express = require('express');  
const fs = require('fs');  
const path = require('path');  
const app = express();  
const uploadDir = path.join(__dirname, 'uploads');  
  
app.post('/upload', (req, res) => {  
  const { file, start, end } = req.body;  
  const fileName = path.join(uploadDir, `file_${Date.now()}`);  
  const fileStream = fs.createWriteStream(fileName, { start, flags: 'a' }); // Append mode  
  
  file.pipe(fileStream);  
  
  fileStream.on('finish', () => {  
    console.log(`Chunk ${start}-${end} saved`);  
    res.send('Chunk uploaded successfully');  
  });  
  
  fileStream.on('error', error => {  
    console.error('Error saving chunk:', error);  
    res.status(500).send('Upload failed');  
  });  
});  
  
app.listen(3000, () => {  
  console.log('Server is running on port 3000');  
});

144、IE事件流和DOM事件流的区别

事件流的区别*

事件执行的顺序不同

  • IE事件流:执行顺序采用冒泡形式,从事件触发的元素开始,逐级冒泡到DOM根节点
  • DOM事件流:支持两种事件模型,即冒泡和捕获,但是捕获先开始,冒泡后发生,捕获从DOM根开始到事件触发元素为止,然后再从事件触发的元素冒泡到 DOM 根,从 DOM 根出发最后又回到了 DOM 根。

监听方式的不同

  • IE事件流:通过attachEventdetachEvent来进行监听与移除
  • DOM事件流:通过addEventListenerremoveEventListener来进行监听与移除

示例:

html
<body>
  <div>
    <button>点击这里</button>
  </div>
</body>

冒泡型事件模型:button->div->body (IE 事件流)
捕获型事件模型:body->div->button (Netscape 事件流)
DOM事件模型:body->div->button->button->div->body (先捕获后冒泡)

事件侦听函数的区别

IE 使用:

js
[Object].attachEvent("name_of_event_handler", fnHandler); //绑定函数
[Object].detachEvent("name_of_event_handler", fnHandler); //移除绑定

DOM 使用:

js
[Object].addEventListener("name_of_event", fnHandler, bCapture); //绑定函数
[Object].removeEventListener("name_of_event", fnHandler, bCapture); //移除绑定

bCapture参数用于设置事件绑定的阶段,true为捕获阶段,false为冒泡阶段。

145、IE和标准下有哪些兼容性的写法

js
// 1、获取事件对象
var ev = ev || window.event;
// 2、事件委派
var target = event.target || event.srcElement;
// 3、获得键盘属性
var cpde = event.which || ebent.keyCode;
// 4、阻止冒泡事件
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true;
// 5、阻止默认行为
event.preventDEfault ? event.preventDEfault() : event.returnValue = false;
// 6、窗口宽高
var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
// 7、滚动条高度
var top = document.documentElement.scrollTop || document.body.scrollTop;
// 8、事件监听
element.addEventListener(), element.attchEvent();
// 9、获取最终生效样式
window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle;

146、写一个通用的事件侦听器函数。

js
const EventUtils = {
    // 视能力分别使用dom0||dom2||IE方式 来绑定事件
    // 添加事件
    addEvent: function(element, type, handler) {
        if (element.addEventListener) {
            element.addEventListener(type, handler, false);
        } else if (element.attachEvent) {
            element.attachEvent("on" + type, handler);
        } else {
            element["on" + type] = handler;
        }
    },

    // 移除事件
    removeEvent: function(element, type, handler) {
        if (element.removeEventListener) {
            element.removeEventListener(type, handler, false);
        } else if (element.detachEvent) {
            element.detachEvent("on" + type, handler);
        } else {
            element["on" + type] = null;
        }
    },

    // 获取事件目标
    getTarget: function(event) {
        return event.target || event.srcElement;
    },

    // 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
    getEvent: function(event) {
        return event || window.event;
    },

    // 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
    stopPropagation: function(event) {
        if (event.stopPropagation) {
            event.stopPropagation();
        } else {
            event.cancelBubble = true;
        }
    },

    // 取消事件的默认行为
    preventDefault: function(event) {
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    }
};

147、Ajax 是什么? 如何创建一个 Ajax?

Ajax全称是asychronous javascript and xml,可以说是已有技术的组合,主要用来实现客户端与服务器端的异步交互,实现页面的局部刷新。

指的是通过 JavaScript 的异步通信,从服务器获取XML文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

具体来说,AJAX 包括以下几个步骤:

  1. 创建 XMLHttpRequest 对象,也就是创建一个异步调用对象
  2. 创建一个新的 HTTP 请求,并指定该 HTTP 请求的方法、URL 及验证信息
  3. 设置响应 HTTP 请求状态变化的函数
  4. 发送 HTTP 请求
  5. 获取异步调用返回的数据
  6. 使用 JavaScript 和 DOM 实现局部刷新

一般实现:

js
const SERVER_URL = "/server";

let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

cookie 是服务器提供的一种用于维护会话状态信息的数据,通过服务器发送到浏览器,浏览器保存在本地,当下一次有同源的请求时,将保存的 cookie 值添加到请求头部,发送给服务端。这可以用来实现记录用户登录状态等功能。cookie 一般可以存储 4k 大小的数据,并且只能够被同源的网页所共享访问。

在发生 xhr 的跨域请求的时候,即使是同源下的 cookie,也不会被自动添加到请求头部,除非显示地规定。

149、模块化开发怎么做?

JavaScript 模块化是一种用于组织和管理代码的方式,它可以将代码分解为独立的模块,每个模块都具有明确的接口和功能。以下是在 JavaScript 中实现模块化开发的几种常见方式:

  1. CommonJS:Node.js 使用 CommonJS 规范实现模块化。使用module.exports导出模块,使用require()导入模块。
js
// math.js
module.exports = {
  add: function(num1, num2) {
    return num1 + num2;
  },
  subtract: function(num1, num2) {
    return num1 - num2;
  }
};
// index.js
const math = require('./math');
console.log(math.add(2, 3)); // 输出 5
  1. ES6 模块化:使用export导出模块,使用import导入模块。
js
// math.js
export function add(num1, num2) {
  return num1 + num2;
}
export function subtract(num1, num2) {
  return num1 - num2;
}
// index.js
import { add } from './math';
console.log(add(2, 3)); // 输出 5
  1. AMD模块化:用于浏览器端异步加载模块。使用define()定义模块,使用require()加载模块。
js
// math.js
define(function() {
  return {
    add: function(num1, num2) {
      return num1 + num2;
    },
    subtract: function(num1, num2) {
      return num1 - num2;
    }
  };
});
// index.js
require(['math'], function(math) {
  console.log(math.add(2, 3)); // 输出 5
});

150、js的几种模块规范?

CommonJS

这是Node.js默认的模块规范。它使用require方法来导入模块,使用module.exportsexports来导出模块。CommonJS是同步加载模块,适用于服务器端开发,但不适用于浏览器环境,因为它依赖于Node.js的运行环境。

AMD

AMD是一种异步加载模块的规范,主要用于浏览器环境。它使用define函数来定义模块,并通过回调函数来加载依赖。AMD的实现包括RequireJS。

CMD

CMD也是一种主要用于浏览器环境的模块规范,与AMD类似,但它采用了依赖就近的原则,只在需要的时候才去require。CMD的实现包括SeaJS。

UMD

UMD是一种兼容多种模块规范的方式,它可以在CommonJS、AMD和全局变量等多种环境中运行。UMD先判断当前环境支持哪种模块规范,然后采用相应的模块规范来加载模块。

ES6 Modules

ES6引入了原生的模块系统,使用importexport关键字来导入和导出模块。ES6模块是静态的,只能在顶层使用importexport,不能在函数或条件语句内部使用。

151、JavaScript 类数组对象的定义?

JavaScript类数组对象是一个拥有数字索引length属性的对象,它类似于一个数组,但并不具备完全的数组特性。

例如,它没有数组原型上的方法,如push()pop(),但可以通过下标访问元素。

常见的类数组对象包括函数参数argumentsDOM元素列表HTMLCollection和属性集合等。

152、为什么0.1 + 0.2 != 0.3?如何解决这个问题?

计算机中所有的数据最终都是以二进制的形式存储的,当然数字的存储也不例外。

当计算0.1+0.2的时候,实际上计算的是这两个数字在计算机里所存储的二进制,0.10.2在转换为二进制表示的时候会出现位数无限循环的情况。

JavaScript中数字的存储:在二进制科学表示法中,双精度浮点的小数部分最多只能保留52 位(比如1.xxx... * 2^n,小数点后的x最多保留52位),加上前面的1,其实就是保留53位有效数字,超过这个长度的位数会被舍去(会采用 0舍1入 的方式),这样就造成了精度丢失的问题

解决办法

  • 由于小数的运算可能导致精度丢失问题,那么要解决这个问题,可以将其转换为整数后再进行运算,运算后再转换为对应的小数,例如:
    js
    var a = 0.1, b = 0.2
    var result = (a * 100 + b * 100) / 100
    console.log(result) // 0.3
    console.log(result === 0.3) // true
    
  • 当然,除了上述方式外,我们也可以利用 ES6 中的极小数Number.EPSILON来进行判断。

    例如判断0.1 + 0.2是否等于0.3,可以将两个数字相加的结果与0.3相减,如果想着的结果小于极小数,那么就可以认定是相等的:

    js
    var a = 0.1, b = 0.2, c = 0.3;
    var result = (Math.abs(a + b - c) < Number.EPSILON);
    console.log(result) // true
    
  • 初次之外还可以使用Decimal.js进行计算
    js
    const Decimal = require('decimal.js');
    const result = new Decimal(0.1).add(0.2); // 0.3
    

浮点数运算可能造成精度丢失的情况,可能造成精度丢失的地方有:

  • 超过有效数字位数时会被舍入处理
  • 运算过程中对阶可能造成精度丢失
  • 运算过程中规格化处理后的舍入处理可能造成精度丢失

0.1 + 0.2由于两次存储时的精度丢失加上一次运算时的精度丢失,所以最终结果0.1 + 0.2 !== 0.3

153、原码、反码和补码的介绍

原码

十进制数据的二进制表现形式就是原码,原码最左边的一个数字就是符号位,0为正,1为负

反码

正数的反码是其本身(等于原码),负数的反码是符号位保持不变,其余位取反

补码

正数的补码是其本身,负数的补码等于其反码+1。因为反码不能解决负数跨零(类似于-6 + 7)的问题,所以补码出现了。

154、jsfor循环注意点

js
for (var i = 0, j = 0; i < 5, j < 9; i++, j++) {
  console.log(i, j);
}

当判断语句含有多个语句时,以最后一个判断语句的值为准,因此上面的代码会执行 10 次。当判断语句为空时,循环会一直进行。

155、怎么做 JS 代码 Error 统计?

error 统计使用浏览器的window.onerror事件。

156、如何确定页面的可用性时间,什么是Performance?

Performance API用于精确度量、控制、增强浏览器的性能表现。这个 API 为测量网站性能,提供以前没有办法做到的精度。

使用getTime来计算脚本耗时的缺点:

  • 首先,getTime方法(以及 Date 对象的其他方法)都只能精确到毫秒级别(一秒的千分之一),想要得到更小的时间差别就无能为力了
  • 其次,这种写法只能获取代码运行过程中的时间进度,无法知道一些后台事件的时间进度,比如浏览器用了多少时间从服务器加载网页。

为了解决这两个不足之处,ECMAScript 5引入“高精度时间戳”这个 API,部署在performance对象上。它的精度可以达到1毫秒的千分之一(1秒的百万分之一)。可以计算出网页加载各个阶段的耗时。

比如,网页加载整个过程的耗时的计算方法如下:

js
var t = performance.timing;
var pageLoadTime = t.loadEventEnd - t.navigationStart;
  • navigationStart:当前浏览器窗口的前一个网页关闭,发生unload事件时的Unix毫秒时间戳。如果没有前一个网页,则等于fetchStart属性。
  • loadEventEnd:返回当前网页load事件的回调函数运行结束时的Unix毫秒时间戳。如果该事件还没有发生,返回0

157、js中倒计时的纠偏实现?

倒计时的纠偏指的是在定时器更新倒计时显示时,由于定时器的不精确性以及JavaScript引擎的性能波动等原因,可能导致倒计时显示时间与实际剩余时间存在一定误差。

在 JavaScript 中,倒计时的纠偏实现可以通过以下步骤完成:

  1. 获取当前时间(可以使用Date.now()方法)和预定结束时间。
  2. 计算两个时间之间的差值,得出剩余的毫秒数。
  3. 使用setTimeoutsetInterval函数来每隔一段时间更新剩余的毫秒数,并更新显示倒计时的 UI。
  4. 为了纠偏误差,可以使用performance.now()方法获取更精确的时间戳,并计算与上次更新的时间戳之间的差距,用此差距来修正下一次更新的时间。
js
const targetTime = new Date('2023-04-01T00:00:00.000Z').getTime(); // 预定结束时间
let remainingTime = targetTime - Date.now(); // 剩余毫秒数

function updateCountdown() {
  const currentTime = performance.now(); // 当前时间戳
  const elapsedTime = currentTime - lastUpdateTime; // 上次更新至今的时间差
  remainingTime -= elapsedTime; // 扣除时间差
  if (remainingTime <= 0) {
    // 倒计时结束
    clearInterval(intervalId);
    return;
  }
  // 更新 UI 显示
  const hours = Math.floor(remainingTime / (1000 * 60 * 60));
  const minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
  const seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);
  countdownEl.innerHTML = `${hours}:${minutes}:${seconds}`;
  lastUpdateTime = currentTime; // 记录此次更新的时间戳
}

let lastUpdateTime = performance.now(); // 上次更新的时间戳
const intervalId = setInterval(updateCountdown, 1000); // 每秒钟更新一次显示

158、ECMAScript 和 JavaScript 的关系

JavaScript 是一种编程语言,而 ECMAScript 是这种语言的标准化规范。ECMAScript 定义了 JavaScript 的语法、类型、语句、关键字等方面的规则和约束。

159、一次js请求一般情况下有哪些地方会有缓存处理?

  1. 浏览器缓存:浏览器会在本地缓存静态资源,例如 JavaScript 文件。如果浏览器已经缓存了该文件,则会从缓存中读取该文件而不是向服务器发起请求。
  2. CDN 缓存:如果该文件被托管在 CDN 上,则可能会使用 CDN 的缓存。CDN 可以在全球多个节点缓存文件,并将请求路由到最近的可用节点。如果一个节点已经缓存了该文件,则会直接返回缓存的文件而不是向原始服务器发起请求。
  3. 代理服务器缓存:如果请求通过代理服务器进行传输,则代理服务器可能会缓存文件并在后续的请求中返回缓存文件而不是向原始服务器发起请求。

160、Array.from()Array.reduce()

  • Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组
  • Array.reduce()方法对累加器和数组中的每个元素 (从左到右)应用一个函数,将其减少为单个值。

Array.from()

js
// 1、将类数组对象转换为真正数组:
let arrayLike = {
  0: "tom",
  1: "65",
  2: "",
  3: ["jane", "john", "Mary"],
  length: 4
};
let arr = Array.from(arrayLike);
console.log(arr); // ['tom','65','男',['jane','john','Mary']]


// 2、改变类数组对象的键值
let arrayLike = {
  name: "tom",
  age: "65",
  sex: "",
  friends: ["jane", "john", "Mary"],
  length: 4
};
let arr = Array.from(arrayLike);
console.log(arr); // [ undefined, undefined, undefined, undefined ]

// 3、将Set结构的数据转换为真正的数组:
let arr = [12, 45, 97, 9797, 564, 134, 45642];
let set = new Set(arr);
console.log(Array.from(set)); // [ 12, 45, 97, 9797, 564, 134, 45642 ]

// Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。如下
let arr = [12, 45, 97, 9797, 564, 134, 45642];
let set = new Set(arr);
console.log(Array.from(set, item => item + 1)); // [ 13, 46, 98, 9798, 565, 135, 45643 ]


// 4、将字符串转换为数组:
let str = "hello world!";
console.log(Array.from(str)); // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "!"]

// 5、Array.from参数是一个真正的数组:
console.log(Array.from([12, 45, 47, 56, 213, 4654, 154]));
// 像这种情况,Array.from会返回一个一模一样的新数组

161、ES6 如何动态加载 import

js
import("lodash").then(_ => {
  // Do something with lodash (a.k.a '_')...
});

162、箭头函数和普通函数有什么区别

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象,用call apply bind也不能改变this指向
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。
  • 箭头函数没有原型对象prototype

163、僵尸进程和孤儿进程是什么?

  • 孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些子进程都会成为孤儿进程。孤儿进程将被init 进程(进程号为1)所收养,并由 init 进程对它们完成状态收集工作。
  • 僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用的资源,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。

164、点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?

  • 点击刷新按钮或者按F5:浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是200。
  • 用户按Ctrl+F5(强制刷新):浏览器不仅会对本地文件过期,而且不会带上If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200。
  • 地址栏回车:浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。

165、git pull和git fetch的区别?

  • git fetch只是将远程仓库的变化下载下来,并没有和本地分支合并。
  • git pull会将远程仓库的变化下载下来,并和当前分支合并。

166、git rebase和git merge的区别?

git mergegit rebase都是用于分支合并,关键在commit记录的处理上不同:

  • git merge会新建一个新的commit对象,然后两个分支以前的commit记录都指向这个新commit记录。这种方法会保留之前每个分支的 commit 历史。
  • git rebase会先找到两个分支的第一个共同的commit 祖先记录,然后将提取当前分支这之后的所有commit记录,然后将这个commit 记录添加到目标分支的最新提交后面。经过这个合并后,两个分支合并后的 commit 记录就变为了线性的记录了。

Released under the MIT License.