Skip to content
js 的基本数据类型?
  • Number:用于表示数字,包括整数和浮点数
  • String:用于表示文本或字符序列
  • Boolean:用于表示逻辑值
  • Null:表示一个空值或“无”的值。只有一个值:null
  • Undefined:当变量被声明了,但没有赋值时,它的值就是 undefined
  • Symbol:表示一个唯一的、不可变的数据类型,通常用于对象属性的键
  • BigInt:用于表示任意大小的整数。当数值超过 Number.MAX_SAFE_INTEGER 或小于 Number.MIN_SAFE_INTEGER 时,可以使用 BigInt
什么是变量提升?

变量提升(Variable Hoisting)是代码执行前,js引擎会先处理变量声明函数声明,将它们提升到当前作用域(函数作用域或全局作用域)的顶部, 并再开始执行代码。这就是所谓的“变量提升”。

然而,需要注意的是,虽然变量声明会被提升,但变量的赋值部分并不会被提升。因此,如果在声明变量之前就尝试访问该变量的值,结果将是undefined

js
console.log(a); // 输出:undefined
var a = 'bar';

需要注意的是,在实际开发中,使用letconst声明的变量并不会发生变量提升,它们具有块级作用域,并且存在暂时性死区(Temporal Dead Zone,TDZ)。因此,推荐使用letconst来声明变量,以避免由于变量提升可能导致的潜在问题。

js中的原型,原型链分别是什么?

每个对象(object)都有一个私有属性(__proto__)指向另一个名为原型(prototype)的对象。

原型链是 js 中对象继承属性的一个主要机制。在 js 中,每个对象都有一个指向它的原型(prototype)对象的内部链接。这个原型对象自身也可能有原型,这样就形成了一个链条,一直连接到最终为 null 的地方,这个链条就被称为原型链。

js
// JS 代码还没运行的时候,JS 环境里已经有一个 window 对象了
// window 对象有一个 Object 属性,window.Object 是一个函数对象
// window.Object 这个函数对象有一个重要属性是 prototype
// window.Object.prototype 里面有这么几个属性 toString(函数)、valueOf(函数)

const arr = [1, 2, 3];
// 在调用 arr.push 的时候,arr 自身没有 push 属性,就去 arr.__proto__ 上找 push
// 因此 arr.push 实际上是 window.Array.prototype.push
arr.push(1);

// arr 自身没有 valueOf,
// 于是去 arr.__proto__ 上找arr.__proto__ 只有 pop、push 也没有 valueOf
// 于是去 arr.__proto__.__proto__ 上找 arr.__proto__.__proto__ 就是 window.Object.prototype
// 所以 arr.valueOf 其实就是 window.Object.prototype.valueOf
console.log(arr.valueOf())


1..__proto__ === Number.prototype

'1'.__proto__ === String.prototype

true.__proto__ === Boolean.prototype


function Fn() {}
var obj = new Fn()
console.log(obj.__proto__ === Fn.prototype)
// -> true
console.log(obj.__proto__.__proto__ === Object.prototype)
// -> true
console.log(obj.__proto__.__proto__.__proto__ === null)
// -> true

function Person() {}
var person = new Person()
console.log(person.__proto__ === Person.prototype)//true
console.log(Person.prototype.constructor===Person)//true

console.log(Object.getPrototypeOf(person) === Person.prototype) // true
说说 new 操作符具体干了什么?
  • 在内存中创建一个新对象
  • 将新对象的 __proto__ 被赋值为构造函数的 prototype 属性
  • 将构造函数中的 this 指向新对象
  • 执行构造函数中的代码
  • 如果构造函数返回非空对象,则返回该对象;否则返回刚创建的新对象
js

function _new (_constructor, ...args) {
  const obj = Object.create(null)
  obj.__proto__ = _constructor.prototype
  const res = _constructor.call(obj, ...args)
  return res instanceof Object ? res : obj
}
说说你对事件循环的理解

事件循环是 js 运行时环境的一个重要组成部分,它主要用于处理异步事件回调函数

在 js 代码执行时,所有同步任务会立即执行,而异步任务则会被放入事件队列中等待执行。当所有同步任务执行完毕后,事件循环会从事件队列中取出一个任务,并将其放入调用栈中执行。当该任务执行完毕后,事件循环会再次从事件队列中取出下一个任务,并重复这个过程。

事件循环的组成主要包括:调用栈任务队列事件循环线程

  • 调用栈用来管理代码的执行顺序
  • 任务队列用来存放异步任务
  • 而事件循环线程则是一个循环,不断地从任务队列中取出任务执行

宏任务:

  • 整体的代码script、setTimeout、setInterval、setImmediate、I/O、UI渲染等

微任务:

  • await后面的代码, Promise.then、process.nextTick、MutationObserver等。
js
console.log('Start');

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

Promise.resolve().then(function() {
  console.log('Promise');
});

console.log('End');

// 输出"Start"
// 将 setTimeout 函数的回调函数添加到宏任务队列中
// 将 Promise.then 回调函数添加到微任务队列中
// 输出"End"
// 此时,执行栈为空,事件循环会检查微任务队列,执行微任务中的函数,输出 "Promise"
// 最后,事件循环会执行宏任务队列中的任务,输出"setTimeout"



async function a() {
  console.log('a')

  await b()

  // await 后面的代码相当于 promise.then
  console.log('a end')
  console.log('c')
}

async function b() {
  console.log('b')
}

a()

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


// new Promise为立即执行函数
new Promise(function (resolve, reject) {
  console.log('promise')
  resolve()
}).then(function () {
  // 微任务
  console.log('then')
})

console.log('main end')

// a
// b
// promise
// main end
// a end
// c
// then
// setTimeout
bind、call、apply 有什么区别?

bind、call和apply都是 js 中用于改变函数执行上下文(即this指向)的方法,但它们之间存在一些重要的区别

执行方式:

  • call 和 apply 立即执行
  • bind 不会立即执行,而是返回一个新的函数。这个新的函数在被调用时,其 this 会被设置为 bind 的第一个参数

传参方式:

  • call 和 bind 在传递参数时,都是将参数逐一传入
  • apply则使用数组或类似数组的对象来传递参数,这意味着你可以使用扩展运算符(...)或数组的 slice 等方法来传递参数

修改this的性质:

  • call 和 apply 是临时修改 this 指向,即只在调用时改变 this 的指向,调用完成后,this 指向会恢复为原样
  • bind 会返回一个新的函数,这个新的函数无论何时调用,其 this 都指向 bind 的第一个参数
js
// 不会立即执行,需要手动调用
const LOG = console.log.bind(console);
闭包?

当一个函数内部返回另一个函数时,这个函数可以访问到父级函数的作用域,这时就形成了一个闭包。

  • 数据封装和私有变量

  • 实现装饰器/函数修饰器

js
function loggingDecorator(fun) {
  return function() {
    console.log(`Calling function: ${fun.name}`);
    const result = fun.apply(this, arguments);
    console.log(`Function returned: ${result}`);
    return result;
  };
}

const f = function() { return 'Hello, world!'; };
const loggedFun = loggingDecorator(f);
console.log(loggedFun());
  • 实现记忆化函数:
js
function memoize(fn) {
  const cache = new Map();
 
  return function(...args) {
    const argsStr = JSON.stringify(args);
    cache.has(argsStr) || cache.set(argsStr, fn.apply(this, args));
    return cache.get(argsStr);
  };
}

function add(a, b) {
  console.log('Calculating...');
  return a + b;
}

const memoizedAdd = memoize(add);

console.log(memoizedAdd(1, 2)); // Calculating...
console.log(memoizedAdd(1, 2)); // 不会再计算
  • 封装循环和定时器
js
function createTimers() {
  for (let i = 0; i < 5; i++) {
    (function(index) {
        setTimeout(function() {
          console.log(`Timer ${index} fired!`);
        }, index * 1000);
    })(i);
  }
}

createTimers();
defineProperty和Proxy有什么区别?

defineProperty 是 ES5 中的方法,它可以直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。vue.js则是采用数据劫持结合发布者 - 订阅者模式的方式,通过 Object.defineProperty() 劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

js
var log = console.info.bind(console) , obj = {}
var defineReactive = (obj, key)=> {
  Object.defineProperty(obj, key, {
    get:(v)=> {
      log(`this is get method , value is ${v}`) 
    },
    set:(v)=> {
      log(`this is set method , newValue is ${v}`)   
    }
  })
}


defineReactive(obj,'name')
obj.name = 'Chiang'
obj.name

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

js
var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
Javascript如何实现继承?

在 js 中,有多种方式可以实现继承,下面列举了几种常见的方法:

  • 原型链继承
javascript
Date.prototype.format = function (date, format) {
};

const date = new Date();
date.format();
  • 构造函数继承:
js
function Parent() {
  this.name = 'parent';
  this.colors = ['red', 'blue', 'green'];
}

function Child() {
  Parent.call(this); // 继承 Parent
  this.type = 'child';
}

var child1 = new Child();
console.log(child1.name); // parent
  • ES6 的 class 继承:
javascript
class Parent {
  constructor() {
    this.name = 'parent';
  }

  getName() {
    return this.name;
  }
}

class Child extends Parent {
  constructor() {
    super(); // 调用父类的 constructor
    this.type = 'child';
  }
}

let child1 = new Child();
console.log(child1.getName()); // parent

这种继承方式在语法上更接近于传统的面向对象编程,使得代码更易于理解和维护。

谈谈对 this 对象的理解

它代表函数运行时自动生成的一个内部对象,只能在函数内部使用。this 的值取决于函数被调用的方式,而不是函数被定义的方式。

以下是一些 this 绑定的主要场景:

  • 全局上下文:在全局执行上下文中(在浏览器中的全局对象是 window),this 指向全局对象。
javascript
console.log(this === window); // 输出:true
  • 函数直接调用:如果函数是直接调用的,那么 this 绑定到全局对象。在严格模式下('use strict'),thisundefined
javascript
function fun() {
  console.log(this);
}

fun(); // 输出:window 或 undefined(在严格模式下)
  • 作为对象的方法调用:当函数作为对象的方法被调用时,this 绑定到该对象。
javascript
const obj = {
  fun: function() {
    console.log(this);
  }
};

obj.fun(); // 输出:obj 对象
  • 构造函数调用:当函数用 new 关键字调用时,它作为构造函数,this 绑定到新创建的对象实例。
javascript
function MyConstructor() {
  this.property = 'Hello World';
}

const instance = new MyConstructor();
console.log(instance.property); // 输出:'Hello World'
  • 箭头函数:箭头函数不绑定自己的 this,它会捕获其所在上下文的 this 值,作为自己的 this 值。
javascript
const obj = {
  action: function() {
    const fun = () => {
      console.log(this);
    };
    fun(); // 输出:obj 对象
  }
};

obj.action();
  • 通过 callapplybind 方法调用:这些方法允许你显式地指定函数调用时的 this 值。
javascript
function fun() {
  console.log(this);
}

const obj = {};
fun.call(obj); // 输出:obj 对象
typeof 与 instanceof 有什么区别 ?

typeof 是一个一元操作符,它返回的是数据类型。它可以用于检测原始类型(如 numberstringbooleanundefinedsymbolobject)和函数。

然而,需要注意的是,typeof 对于所有的对象(包括数组和 null)都返回 object。这意味着它不能用来区分数组、null 和其他对象。

js
console.log(typeof 123);            // "number"
console.log(typeof "hello");        // "string"
console.log(typeof true);           // "boolean"
console.log(typeof undefined);      // "undefined"
console.log(typeof Symbol());       // "symbol"
console.log(typeof {});             // "object"
console.log(typeof []);             // "object"
console.log(typeof null);           // "object"
console.log(typeof function() {});  // "function"

instanceof 操作符用于测试构造函数的 prototype 属性是否出现在对象的原型链上。它用来检测一个对象是否是一个已知构造函数的实例。

js
const arr = [];
console.log(arr instanceof Array); // true

const date = new Date();
console.log(date instanceof Date); // true

function MyObject() {}
const obj = new MyObject();
console.log(obj instanceof MyObject); // true
  • typeof 主要用于检测变量的原始类型。
  • instanceof 主要用于检测对象是否由特定的构造函数创建。
高阶函数

高阶函数是指接收函数作为参数或者将函数作为输出结果的函数。在 js 中,高阶函数可以用来实现高阶函数式编程的特性,例如:map、reduce、filter、sort等。

为什么 js 是单线程?

同一个时间只能做一件事,作为浏览器脚本语言,js的主要用途是与操作DOM以及和用户交互。这决定了它只能是单线程,否则会带来很复杂的同步问题

Object.create 和 new 有什么区别?
js
const Person = function(){};
const p1 = Object.create(Person);
const p2 = new Person();

o = Object.create(null);
// 等价于:
o = { __proto__: null };

Object.create 失去了原来对象的属性的访问

DOM 事件模型是什么?
  • DOM0:事件模型是早期的事件模型,所有的浏览器都是支持的, 就是直接在dom对象上注册事件名称。
  • DOM2:事件捕获和事件冒泡。DOM2级中使用 addEventListenerremoveEventListener注册解除事件。
    • preventDefault -- 取消事件的默认行为
    • stopPropagation -- 取消事件进一步捕获或者冒泡
什么是事件代理,以及它的应用场景有哪些?
html
<ul id="ul">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>
  • 减少整个页面所需的内存,提升整体性能
  • 动态绑定,减少重复工作
什么是作用域链?

作用域链(Scope Chain)是 js 中用于查找变量和函数的一种机制。每个 js 函数都会创建一个作用域链。

箭头函数和普通函数有啥区别? 箭头函数能当构造函数吗?
  • 箭头函数不会创建自己的this,它从自己的作用域链的上一层继承this
  • 箭头函数继承而来的 this 指向永远不变
  • 箭头函数不能作为构造函数使用
  • 箭头函数没有arguments
  • 箭头函数没有原型prototype
ajax
js
  xhr = new XMLHttpRequest     // 创建ajax对象
  xhr.open(method,url,async)   // 规定请求地址
  xhr.onload                   // 等待服务器相应
  xhr.send()                   // 向服务器发送请求
谈谈 Javascript 中的类型转换机制

常见的类型转换有:

  • 显示转换
js
Number()
parseInt()
String()
Boolean()
  • 隐式转换
    • 比较运算(==、!=、>、<)、if、while需要布尔值地方
    • 算术运算(+、-、*、/、%)
你是怎么理解ES6中 Decorator 的?使用场景有哪些?

装饰者模式就是一种在不改变原类, 不使用继承的情况下,动态地扩展对象功能

怎么理解ES6中 Generator?使用场景有哪些?

执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态

形式上,Generator 函数是一个普通函数,但是有两个特征:

  • function 关键字与函数名之间有一个星号
  • 函数体内部使用 yield 表达式,定义不同的内部状态
你是怎么理解 ES6 中 Promise的?使用场景有哪些?

在ES6中,Promise是一种用于处理异步操作的对象。它代表了异步操作最终的状态及其结果值

Promise具有以下三个状态:

  • Pending(进行中)
  • Fulfilled(已成功)
  • Rejected(已失败)

Promise 的主要应用场景:

  • 异步操作:处理异步操作, 避免回调地狱
  • 多个异步操作的依赖关系:通过链式调用的方式,我们可以确保操作的顺序和依赖关系得到正确处理。
  • 错误处理:Promise 提供了 catch 方法来捕获异步操作中的错误,并进行相应的处理。这使得错误处理更加方便和统一,避免了传统回调函数中错误处理的混乱和不易管理的问题。

Promise 方法,如.all()、.allSettled()、.race()、.resolve()、.reject()

Map 和 WeakMap

Map和WeakMap之间的主要区别体现在以下几个方面:

  • 键的类型和限制:

    • Map的键可以是任何类型的值,包括基本数据类型(如字符串、数字)和对象类型。
    • 而 WeakMap 的键则必须是对象类型,因为只有对象才有引用。如果尝试使用基本数据类型作为 WeakMap 的键,会导致 TypeError 异常。
  • 垃圾回收机制的影响:

    • 在 Map 中,键和值都会被常规的垃圾回收机制回收。
    • WeakMap的键是弱引用,这意味着当对象被垃圾回收时,WeakMap 中对应的键值对也会被自动删除。这种特性使得 WeakMap 特别适用于缓存或元数据场景,当对象不再被使用时,WeakMap 可以自动清除对应的数据,避免内存泄漏。
  • 遍历能力:

    • Map对象有内置的迭代器,因此可以通过 for…of 循环来遍历键值对。
    • WeakMap没有内置的迭代器,因此不能直接遍历键值对。
  • 方法差异:

    • 虽然 Map 和 WeakMap 都有get、set、has和delete这四个方法用于操作键值对。
    • 但Map还有一些其他方法,如keys、values、entries和forEach,用于遍历键值对, WeakMap则没有这些方法。
Set 和 WeakSet

WeakSet和Set在js中都是用于存储唯一值集合类型,但它们之间存在几个关键的区别:

  • 成员类型:

    • Set 集合的成员可以是任意类型的值,无论是原始类型(如字符串、数字)还是对象类型。Set并不限制其成员的类型。
    • WeakSet 则不同,它的成员只能是对象类型。它不能包含任何原始类型的值。这意味着,如果你尝试向WeakSet中添加一个非对象类型的值,操作将会失败。
  • 引用方式:

    • Set中的对象使用强引用,即只要对象被 Set 引用,它就不会被垃圾回收机制回收,即使没有其他地方引用这个对象。
    • WeakSet中的对象则是弱引用。这意味着,如果没有其他地方引用 WeakSet 中的对象,那么这些对象会被垃圾回收机制自动回收。这种弱引用特性使得 WeakSet 特别适用于缓存或存储临时数据的场景,因为它可以自动清理不再需要的对象,避免内存泄漏。
  • 方法和属性:

    • Set和 WeakSet 都提供了类似的方法,如 add() 用于添加元素,delete()用于删除元素,has() 用于检查元素是否存在。
    • WeakSet专注于对象存储和弱引用,它没有提供size属性来查询集合的大小,因为计算大小可能需要遍历整个集合,这可能会破坏弱引用的特性。
  • 应用场景:

    • Set 支持任意类型的值,并且使用强引用,它适用于需要存储和管理唯一值集合的多种场景,如列表去重、检查元素是否存在等。
    • WeakSet 则更适合于存储对象引用,特别是在不希望因 Set 的强引用而干扰垃圾回收机制的情况下。例如,当你想存储对DOM元素的引用,但又不想阻止这些元素在不再需要时被垃圾回收时,WeakSet就是一个很好的选择。
Javscript 数组的常用方法有哪些?
  • push(): 在数组末尾添加一个或多个元素,并返回数组的长度。
  • pop(): 移除并返回数组末尾的元素, 会改变原始数组。
  • splice(): 从指定位置删除或替换元素,可修改原始数组。
  • reverse(): 颠倒数组中元素的顺序,会修改原始数组。
  • sort(): 对数组中的元素进行排序,默认按照字母顺序排序,会修改原始数组。
  • unshift(): 在数组开头添加一个或多个元素,并返回数组的长度。
  • shift(): 移除并返回数组开头的元素,会改变原始数组。
  • concat(): 合并两个或更多数组,并返回新的合并后的数组,不会修改原始数组。
  • slice(): 从数组中提取指定位置的元素,返回一个新的数组,不会修改原始数组。
  • indexOf(): 查找指定元素在数组中的索引,如果不存在则返回-1。
  • lastIndexOf(): 从数组末尾开始查找指定元素在数组中的索引,如果不存在则返回-1。
  • includes(): 检查数组是否包含指定元素,返回一个布尔值。
  • join(): 将数组中的所有元素转为字符串,并使用指定的分隔符连接它们。
  • filter(): 创建一个新数组,其中包含符合条件的所有元素。
  • map(): 创建一个新数组,其中包含对原始数组中的每个元素进行操作后的结果。
  • reduce(): 将数组中的元素进行累积操作,返回一个单一的值。
  • forEach(): 对数组中的每个元素执行提供的函数。
阻止事件冒泡和默认事件?
  • e.stopPropagation: 阻止事件冒泡

    • 不让事件向 document 上冒泡,但是默认事件任然会执行,当你调用这个方法的时候,如果点击一个连接,这个连接仍然会被打开,
  • e.preventDefault: 阻止默认事件

    • 比如在 a 标签的绑定事件上调用此方法,链接则不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素;
说说 Javascript 为什么会存在数字精度丢失的问题,以及如何进行解决?

js 存在数字精度丢失的问题,主要是因为 js 中所有的数字都是以64位浮点数形式存储的, 是双精度格式(double precision)。这种格式可以表示非常大或非常小的数,但是并不适合表示精确的小数

在二进制浮点数中,有些十进制小数无法精确表示,因此会存在舍入误差。例如,0.1 在二进制中是一个无限循环小数,因此无法精确表示。当我们在 js 中进行数学运算时,这些误差可能会累积,导致最终结果出现明显的精度问题。

解决 js 数字精度丢失问题的方法有以下几种:

  • 使用整数运算:尽可能避免小数运算,尤其是在需要高精度的场景中。如果必须使用小数,可以尝试将其转换为整数进行计算,然后再转换回小数。例如,可以将货币值以分为单位存储和计算,而不是以元为单位。

  • 使用第三方库:有一些 js 库专门用于处理高精度数字运算,如 decimal.js、big.js 等。这些库提供了高精度的数字类型和运算方法,可以有效避免精度丢失问题。

  • 固定小数位数:如果只需要处理固定小数位数的数字(如货币),可以在运算结束后使用 toFixed() 方法将结果四舍五入到指定的小数位数。但需要注意的是,toFixed() 方法返回的是字符串,而不是数字。

  • 避免连续运算:尽量减少连续运算的次数,因为每次运算都可能引入新的舍入误差。可以尝试将多个运算步骤合并为一个步骤,或者使用中间变量存储运算结果。

js
// false
0.1 + 0.1 === 0.2
线程和进程的区别是什么?

做个简单的比喻:进程 = 火车,线程 = 车厢:

  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 线程在进程下行进(单纯的车厢无法运行)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉可能将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存(比如火车上的洗手间)—— "互斥锁"
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)—— “信号量”
内存分配 ?
  • 堆(heap)

    • 用于复杂数据类型(引用类型)分配空间,例如数组对象、object对象, 它是运行时动态分配内存的,因此存取速度较慢
  • 栈(stack)

  • 存放一些基本类型的变量和对象的引用(包含引用类型的值的变量并不是对象本身,而是一个指向该对象的指针,复制该对象的话,实际上是复制该对象的指针),其优势是存取速度比堆要快,并且栈内的数据可以共享,但缺点是存在栈中的数据大小生存期必须是确定的,缺乏灵活性。
try...catch 可以捕获到异步代码中的错误吗?
js
try {
  setTimeout(() => {
    throw new Error("err");
  }, 200);
} catch (err) {
  console.log(err);
}
// setTimeout是一个异步函数,它的回调函数会在指定的延时后被放入事件队列,等待当前执行栈清空后才执行。
// 因此,当setTimeout的回调函数执行并抛出错误时,try...catch已经执行完毕,无法捕捉到异步回调中的错误。
多个 tab 页相互通信,怎么做?
  • 使用 LocalStorage
  • Broadcast Channel API
  • SharedWorker
  • PostMessage
  • websocket
如何确保你的构造函数只能被 new 调用,而不能被普通调用?
  • 借助 instanceof 和 new 绑定的原理,适用于低版本浏览器
js
function Person(firstName, lastName) {
  if (!(this instanceof Person)) {
    throw new TypeError(
      'Function constructor A cannot be invoked without "new"'
    );
  }
}
  • 借助 new.target 属性,可与 class 配合定义抽象类
js
function Person() {
  if (!new.target) {
    throw new TypeError(
      'Function constructor A cannot be invoked without "new"'
    );
  }
}
  • 面向对象编程使用 ES6 Class
立即执行函数的理解

JS立即执行函数模式是一种语法,可以让你的函数在定义后立即被执行,这种模式本质上就是函数表达式(命名的或者匿名的),在创建后立即执行。

js
(function (test) {
  console.log(test);
})(123)
  • 不必为函数命名,避免污染全局变量
  • 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
JSONP 是什么?

jsonp是一种数据调用的方式,带 callbackjson 就是jsonp。下面是一个简单的 JSONP 使用示例:

客户端(js):

javascript
function handleResponse(data) {
  console.log(data); // 处理返回的数据
}

var script = document.createElement('script');
script.src = 'http://example.com/api?callback=handleResponse';
document.body.appendChild(script);

在这个例子中,客户端定义了一个handleResponse函数,用于处理从服务器返回的数据。然后,创建了一个新的<script>标签,并设置其src属性为服务器API的URL,同时在URL的查询字符串中包含了callback参数,其值为客户端定义的回调函数名handleResponse。最后,将<script>标签添加到页面的<body>元素中,这会导致浏览器发起一个GET请求到指定的URL。

服务端(假设使用Node.js和Express):

javascript
const express = require('express');
const app = express();

app.get('/api', (req, res) => {
  const callback = req.query.callback;
  const data = { name: 'John', age: 30 };
  const jsonpData = `${callback}(${JSON.stringify(data)})`;
  res.setHeader('Content-Type', 'application/javascript');
  res.send(jsonpData);
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

在这个服务端示例中,当收到GET请求时,服务器会检查查询字符串中的callback参数,然后将数据转换为JSON格式,并把它包装在回调函数的调用中。这样,返回给客户端的内容实际上是一段js代码,当浏览器执行这段代码时,就会调用客户端定义的handleResponse函数,并传入JSON数据作为参数。

完整流程:

  1. 客户端js代码创建<script>标签,并设置其src属性为服务器的API URL,同时带上回调函数名。
  2. 浏览器解析并执行<script>标签,向服务器发起GET请求。
  3. 服务器收到请求,解析出回调函数名,将数据转换为JSON格式,并包装在回调函数的调用中,返回给客户端。
  4. 浏览器接收到服务器返回的js代码,并执行它。
  5. 执行返回的js代码时,调用客户端定义的回调函数,并传入JSON数据作为参数。
  6. 客户端的回调函数处理接收到的数据。

需要注意的是,由于 JSONP 依赖于<script>标签加载,它只支持 GET 请求,且存在安全风险,如 XSS 攻击。因此,在使用 JSONP 时,需要确保服务器端返回的内容是安全的,并避免在敏感操作中使用它。同时,现代 Web 开发中,更推荐使用 CORS(跨源资源共享)来处理跨域请求。

fetch ?

Fetch API 提供了一个 js接口,用于访问操纵 HTTP 管道的部分,例如请求响应。它还提供了一个全局 fetch()方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。

js
var url = 'https://example.com/profile'
var data = {username: 'example'}
fetch(url, {
  method: 'POST', // or 'PUT'
  body: JSON.stringify(data), // data can be `string` or {object}!
  headers: new Headers({
    'Content-Type': 'application/json'
  })
}).then(
  res => res.json()
)
.catch(error => console.error('Error:', error))
.then(response => console.log('Success:', response))
说说 sourcemap 的原理?

调试原始源代码会比浏览器下载的转换后的代码更加容易。 source map 是从已转换的代码映射到原始源的文件,使浏览器能够重构原始源并在调试器中显示重建的原始源。source map 只有在打开dev tools的情况下才会开始下载,一般不会影响网页性能。

怎么实现图片懒加载?
  • img 标签加上 loading="lazy"<img src="./example.jpg" loading="lazy" />

  • IntersectionObserver

js
const images = document.querySelectorAll("img[data-src]");

const config = {
  rootMargin: "0px",
  threshold: 0,
};

let observer = new IntersectionObserver((entries, self) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      let img = entry.target;
      let src = img.dataset.src;
      if (src) {
        img.src = src;
        img.removeAttribute("data-src");
      }
      self.unobserve(entry.target);
    }
  });
}, config);

images.forEach((it) => {
  observer.observe(it);
});
如何判断一个元素是否在可视区域中?
  • offsetTop、scrollTop

  • getBoundingClientRect

js
function isInViewPort(element) {
  const doc = document;
  const docEle = doc.documentElement;

  const viewWidth = window.innerWidth || docEle.clientWidth;
  const viewHeight = window.innerHeight || docEle.clientHeight;

  const { top, right, bottom, left } = element.getBoundingClientRect();

  return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight;
}
  • Intersection Observer
js
const options = {
  // 表示重叠面积占被观察者的比例,从 0 - 1 取值,
  // 1 表示完全被包含
  threshold: 1.0, 
  root:document.querySelector('#scrollArea') // 必须是目标元素的父级元素
};

const callback = (entries, observer) => { ....}

const observer = new IntersectionObserver(callback, options);
说说 js 中内存泄漏有哪几种情况?

Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存

通常情况下有两种实现方式:

  • 标记清除
    • 当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“
    • 垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉
    • 在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了
    • 随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存
  • 引用计数
    • 语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

常见内存泄露情况:

  • 意外的全局变量
js
function foo() {
  hero = "this is a hidden global variable";
}

function foo() {
  this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();
  • 定时器也常会造成内存泄露

  • 闭包

js
function bindEvent() {
  var obj = document.createElement('XXX');
  var unused = function () {
    console.log(obj, '闭包内引用obj obj不会被释放');
  };
  obj = null; // 解决方法
}
  • 没有清理对DOM元素的引用同样造成内存泄露
js
const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用
深拷贝浅拷贝有什么区别?怎么实现深拷贝?
  • 浅拷贝(即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址)

  • Object.assign

  • Array.prototype.slice(), Array.prototype.concat()

  • 使用拓展运算符实现的复制

  • 深拷贝

    • jQuery.extend()
    • JSON.stringify() [会忽略undefined、symbol和函数]
    • 手写循环递归
arguments 这种类数组,如何遍历类数组?

Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。

js
function foo(name, age, sex) {
  console.log(arguments);

  console.log("实参的长度为:" + arguments.length);
}

foo('name', 'age', 'sex')

// Arguments对象的length属性,表示实参的长度
// 形参的长度为:3
// 实参的长度为:1
foo(1);


// Arguments 对象的 callee 属性,通过它可以调用函数自身
var data = [];
for (var i = 0; i < 3; i++) {
  (data[i] = function () {
    console.log(arguments.callee.i) 
  }).i = i;
}

data[0]();
data[1]();
data[2]();
// 0
// 1
// 2


// 使用 apply 将 foo 的参数传递给 bar
function foo() {
  bar.apply(this, arguments);
}


function bar(a, b, c) {
  console.log(a, b, c);
}

foo(1, 2, 3)


function func(...arguments) {
  console.log(arguments); // [1, 2, 3]
}

func(1, 2, 3);
怎么实现大型文件上传?
  • 分片上传(Chunked Upload):将大文件拆分成小的文件块(chunk),然后通过多个并行的请求依次上传这些文件块。服务器接收到每个文件块后进行存储,最后合并所有文件块以还原原始文件。这种方法可以降低单个请求的负载,并允许在网络中断或上传失败时可以从断点续传。

  • 流式上传(Streaming Upload):客户端使用流方式逐步读取文件的内容,并将数据流通过 POST 请求发送给服务器。服务器端能够逐步接收并处理这些数据流,而无需等待完整的文件上传完成。这种方法适用于较大的文件,减少了内存占用和传输延迟。

  • 使用专门的文件上传服务:有一些第三方服务可供使用,例如云存储服务(如 Amazon S3、Google Cloud Storage)、文件传输协议(如 FTP、SFTP)等。这些服务通常提供了高可靠性、可扩展性和安全性,并且针对大文件上传进行了优化。

  • 压缩文件上传:如果可能,可以在客户端先对文件进行压缩,然后再进行上传。压缩后的文件大小更小,可以减少上传时间和网络带宽消耗。

  • 并发上传:通过多个并行的请求同时上传文件的不同部分,以加快整个上传过程。这需要服务器端支持并发上传并正确处理分片或部分文件的合并。

  • 断点续传:记录上传进度和状态,以便在网络中断或上传失败后能够从上次中断的位置继续上传。可以使用客户端或服务器端的断点续传机制来实现。

js 中如何取消请求?
  • 取消 XMLHttpRequest 请求
js
const xhr = new XMLHttpRequest();
xhr.open('GET', '<http://127.0.0.1:3000/api/get>', true);
xhr.send();
setTimeout(() => {
	xhr.abort();
}, 1000);
  • 取消 Fetch 请求
js
const controller = new AbortController();
void (async function () {
  const response = await fetch('<http://127.0.0.1:3000/api/get>', {
    signal: controller.signal,
  });
  const data = await response.json();
})();

setTimeout(() => {
  controller.abort();
}, 1000);
  • axios
js
const controller = new AbortController();
const API_URL = '<http://127.0.0.1:3000/api/get>';
void (async function () {
  const response = await axios.get(API_URL, {
    signal: controller.signal,
  });
  const { data } = response;
})();

setTimeout(() => {
  controller.abort();
}, 1000);
map 和 forEach 有什么区别?
  • forEach()方法不会返回执行结果,而是undefined。forEach()会修改原来的数组。
  • map()方法会得到一个新的数组并返回。
如何让 Proxy 去监听基本数据类型?

Proxy无法直接监听基本数据类型(如字符串、数字、布尔值等),因为它们是不可变的。Proxy只能在对象级别上进行操作,而不是基本数据类型。

当我们尝试使用 Proxy 包装基本数据类型时,会得到一个 TypeError 错误,因为基本数据类型不具有属性和方法

js
const value = 'Hello';

const handler = {
  set(target, property, value) {
    console.log(`Setting property '${property}' to '${value}'`);
    target[property] = value;
    return true;
  }
};

const proxyValue = new Proxy(value, handler); // TypeError: Cannot create proxy with a non-object as target

如果要监听基本数据类型的更改,最好使用其他方式,例如通过触发事件或调用回调函数来通知更改。可以创建一个自定义的数据包装器,将基本数据类型包装在对象中,并在该对象上实现监听逻辑。这样,可以在包装器对象上添加 Proxy 以监听其属性的更改。

js
function ValueWrapper(value) {
  this.value = value;
  this.onChange = null;
}

ValueWrapper.prototype.setValue = function(newValue) {
  this.value = newValue;
  if (typeof this.onChange === 'function') {
    this.onChange(this.value);
  }
};

const wrapper = new ValueWrapper('Hello');

const handler = {
  set(target, property, value) {
    console.log(`Setting property '${property}' to '${value}'`);
    target[property] = value;
    if (typeof target.onChange === 'function') {
      target.onChange(target.value);
    }
    return true;
  }
};

const proxyWrapper = new Proxy(wrapper, handler);

proxyWrapper.onChange = newValue => {
  console.log(`Value changed: ${newValue}`);
};

proxyWrapper.setValue('Hi'); // 输出: Setting property 'value' to 'Hi', Value changed: Hi
addEventListener 第三个参数?
js
addEventListener(type, listener); 
addEventListener(type, listener, options); 
addEventListener(type, listener, useCapture);
  • type表示监听事件类型的大小写敏感的字符串

  • options 可选一个指定有关 listener 属性的可选参数对象

    • capture 可选一个布尔值,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发
    • once 可选一个布尔值,表示 listener 在添加之后最多只调用一次。如果为 true,listener 会在其被调用之后自动移除。
    • passive 可选一个布尔值,设置为 true 时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。查看使用 passive 改善滚屏性能以了解更多。
    • signal 可选 AbortSignal,该 AbortSignalabort() 方法被调用时,监听器会被移除
  • useCapture 可选一个布尔值,表示在 DOM 树中注册了 listener 的元素,是否要先于它下面的 EventTarget 调用该 listener。当 useCapture(设为 true)时,沿着 DOM 树向上冒泡的事件不会触发 listener。当一个元素嵌套了另一个元素,并且两个元素都对同一事件注册了一个处理函数时,所发生的事件冒泡和事件捕获是两种不同的事件传播方式。事件传播模式决定了元素以哪个顺序接收事件。进一步的解释可以查看 DOM Level 3 事件及 js 事件顺序文档。如果没有指定,useCapture 默认为 false。

Promise 的 finally 怎么实现的?
js
Promise.prototype.finally = function(callback) {
  const P = this.constructor;
  return this.then(
    value => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
}
Web Worker 是什么?

Web Worker 是 HTML5 标准中提供的一项技术,它可以让 js 脚本在后台线程运行,从而避免阻塞 UI 线程。Web Worker 可以创建一个独立的线程来执行脚本,从而使得主线程可以专注于用户交互和响应。

Web Worker 的主要特点包括:

  • 独立线程:Web Worker 可以在独立的线程中运行 js 代码,从而避免了在主线程中运行耗时任务的风险。
  • 沙箱环境:Web Worker 运行的 js 代码在一个受限的沙箱环境中,不能访问与主线程共享的 DOM、全局变量等资源,从而保证了数据安全性和代码稳定性。
  • 事件通信:Web Worker 可以通过事件来与主线程进行通信,从而实现线程间的数据传递和同步操作。
  • 使用 Web Worker 可以改善因大量 JS 计算导致的卡顿问题,增强页面的稳定性和用户体验。

Web Worker 不仅可以在浏览器中运行,还可以在 Node.js 中运行,在实际应用和开发中都有广泛的应用。

说说对 requestIdleCallback 的理解

requestIdleCallback 是一个浏览器 API,它允许我们在浏览器空闲时执行一些任务,以提高网页的性能和响应速度。

通常情况下,js 代码会占用主线程,从而阻塞了其他的任务。当页面需要进行一些复杂计算、渲染大量的DOM元素等操作时,就会导致用户的交互体验变得缓慢和卡顿。

requestIdleCallback 的作用就是将一些非关键性的任务从主线程中分离出来,等到浏览器闲置时再执行。这样就可以避免占用主线程,提高页面的响应速度和流畅度。

使用 requestIdleCallback 需要传入一个回调函数,该函数会在浏览器空闲时被调用。回调函数的参数是一个 IdleDeadline 对象,它包含有关浏览器还剩余多少时间可供执行任务的信息。根据该对象的时间戳信息,开发人员可以自行决定是否继续执行任务或推迟执行。

var、let、const之间有什么区别?
  • 变量提升
  • 暂时性死区 (let和const存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量)
  • 块级作用域 (let和const存在块级作用域)
  • 重复声明 (let和const在同一作用域不允许重复声明变量)
  • 修改声明的变量 (const声明一个只读的常量。一旦声明,常量的值就不能改变)
说说你对轮询的理解

轮询是指在一定的时间间隔内,定时向服务器发送请求,获取最新数据的过程。轮询通常用于从服务器获取实时更新的数据。

轮询是在固定的时间间隔内向服务器发送请求,即使服务器没有数据更新也会继续发送请求。而长轮询是先发送一个请求,服务器如果没有数据更新,则不会立即返回,而是将请求挂起,直到有数据更新时再返回结果。

轮询会产生大量的无效请求,浪费带宽和服务器资源,并且对服务器的压力比较大。同时,在短时间内频繁地发送请求可能会被服务器视为恶意行为,导致 IP 被封禁等问题。

为了避免轮询的缺点,可以使用 WebSocket、SSE(Server-Sent Events)等技术来实现实时数据更新。

WebSocket 是一种双向通信协议,能够实现服务器与客户端之间的实时通信;而 SSE 则是一种基于 HTTP单向通信协议,可以实现服务器向客户端推送实时数据。

null 和 undefined 有什么区别?

undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。

cookie、localStorage和sessionStorage 三者之间有什么区别

生命周期

  • cookie:可设置失效时间,没有设置的话,默认是关闭浏览器后失效
  • localStorage:除非被手动清除,否则将会永久保存。
  • sessionStorage: 仅在当前网页会话下有效,关闭页面或浏览器后就会被清除。

存放数据大小

  • cookie:4KB左右
  • localStorage和sessionStorage:可以保存5MB的信息。

http请求

  • cookie:每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
  • localStorage和sessionStorage:仅在客户端(即浏览器)中保存,不参与和服务器的通信

从安全性来说,因为每次 http 请求都会携带 cookie 信息,这样无形中浪费了带宽,所以 cookie 应该尽可能少的使用,另外 cookie 还需要指定作用域,不可以跨域调用(当前页面只能读取页面所在域的 cookie,即 document.cookie ),限制比较多。

localStorage 和 sessionStorage 唯一的差别一个是永久保存在浏览器里面,一个是关闭网页就清除了信息。localStorage 可以用来跨页面传递参数,sessionStorage 用来保存一些临时的数据,防止用户刷新页面之后丢失了一些参数。

js函数有哪几种声明方式?有什么区别?

表达式声明式 两种函数声明方式。

js

// 函数提升,所有通过function关键字声明的变量都会被解释器优先编译,不管声明在什么位置都可以调用它,但是它本身并不会被执行。
test(); // 测试
function test() {
  console.log("测试");
}
test(); // 测试

// 不会导致函数提升,必须先声明后调用,不然就会报错。
test(); // 报错:TypeError: test is not a function
var test = function() {
  console.log("测试");
};

区别:

  • 函数声明式变量会声明提前 函数表达式变量不会声明提前
  • 函数声明中的函数名是必需的,而函数表达式中的函数名则是可选的。
  • 函数表达式可以在定义的时候直接在表达式后面加()执行,而函数声明则不可以。
  • 自执行函数即使带有函数名,它里面的函数还是属于函数表达式。
谈谈你对浏览器中进程和线程的理解

它主要包括以下进程:

  • Browser 进程:浏览器的主进程,唯一,负责创建销毁其它进程、网络资源的下载与管理、浏览器界面的展示、前进后退等。
  • GPU 进程:用于 3D 绘制等,最多一个。
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建。
  • 浏览器渲染进程(浏览器内核):内部是多线程的,每打开一个新网页就会创建一个进程,主要用于页面渲染脚本执行事件处理等。

浏览器的渲染进程是多线程的,页面的渲染,js 的执行,事件的循环,都在这个进程内进行:

  • GUI 渲染线程:负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。
  • js 引擎线程:也称为 js 内核,负责处理 Javascript 脚本程序、解析 Javascript 脚本、运行代码等。(例如 V8 引擎)
  • 事件触发线程:用来控制浏览器事件循环,注意这不归 js 引擎线程管,当事件被触发时,该线程会把事件添加到待处理队列的队尾,等待 js 引擎的处理。
  • 定时触发器线程:传说中的 setInterval 与 setTimeout 所在线程
  • 异步 http 请求线程:在 XMLHttpRequest 连接后通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 js 引擎执行。

注意,GUI 渲染线程与 js 引擎线程是互斥的,当 js 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 js 引擎空闲时立即被执行。所以如果 js 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

所谓单线程,是指在 js 引擎中负责解释和执行 js 代码的线程唯一,同一时间上只能执行一件任务。

如何顺序执行10个异步任务?

简单的 for 循环是依次进行循环的,不像 Array.forEach,Array.map 方法是并发执行的,利用这一特点加 async / await 很容易写出下面这样的代码:

js
(async () => {
  const sleep = delay => {
    return new Promise((resolve, reject) => {
      setTimeout(_ => resolve(), delay)
    })
  }
  
  const task = (i) => {
    return new Promise(async (resolve, reject) => {
      await sleep(500)
      console.log(`now is ${i}`)
      ++i
      resolve(i)
    })
  }
  
  let params = -1
  for (let i = 0; i < 4; i++) {
    params = await task(params)
  }  
})()
如何让Promise.all在抛出异常后依然有效

在处理多个并发请求时,我们一般会用 Promise.all() 方法。该方法指当所有在可迭代参数中的 promises 已完成,或者第一个传递的 promise(指 reject)失败时,返回 promise。但是当其中任何一个被拒绝的话。Promise.all([..])就会立即被拒绝,并丢弃来自其他所有 promise 的全部结果。也就是说,promise.all 中任何一个 promise 出现错误的时候都会执行reject,导致其它正常返回的数据也无法使用。

js

var p1 = new Promise((resolve, reject) => {
	resolve('p1');
});

var p2 = new Promise((resolve, reject) => {
	resolve('p2');
});

var p3 = new Promise((resolve, reject) => {
	reject('p3');
});

Promise.all([p1, p2, p3].map(p => p.catch(e => '出错后返回的值' )))
.then(values => {
  console.log(values);
}).catch(err => {
  console.log(err);
})

Promise.allSettled()方法返回一个promise,该promise在所有给定的promise已被解析或被拒绝后解析,并且每个对象都描述每个promise的结果。

直接在script标签中写 export 为什么会报错?

现代浏览器可以支持用 script 标签引入模块或者脚本,如果要引入模块,必须给 script 标签添加type="module"。如果引入脚本,则不需要 type。

窗口宽度,高度
  • clientWidth/clientHeight 返回的是元素的内部宽度,它的值只包含 content + padding,如果有滚动条,不包含滚动条。
  • clientTop 返回的是上边框的宽度。
  • clientLeft 返回的左边框的宽度。
  • offsetWidth/offsetHeight 返回的是元素的布局宽度,它的值包含 content + padding + border 包含了滚动条。
  • offsetTop 返回的是当前元素相对于其 offsetParent 元素的顶部的距离。
  • offsetLeft 返回的是当前元素相对于其 offsetParent 元素的左部的距离。
  • scrollWidth/scrollHeight 返回值包含 content + padding + 溢出内容的尺寸。
  • scrollTop 属性返回的是一个元素的内容垂直滚动的像素数。
  • scrollLeft 属性返回的是元素滚动条到元素左边的距离。
什么是点击穿透,怎么解决?

在发生触摸动作约300ms之后,移动端会模拟产生click动作,它底下的具有点击特性的元素也会被触发,这种现象称为点击穿透。

书写规范问题,不要混用 touch 和 click;消费掉 touch 之后的 click。

xml 和 json 有什么区别?
  • JSON是js Object Notation;XML是可扩展标记语言
  • JSON是基于js语言;XML源自 SGML
  • JSON是一种表示对象的方式;XML是一种标记语言,使用标记结构来表示数据项。
  • JSON不提供对命名空间的任何支持;XML支持名称空间
  • JSON支持数组;XML不支持数组。
  • XML的文件相对难以阅读和解释;与XML相比,JSON的文件非常易于阅读
  • JSON不使用结束标记;XML有开始和结束标签
  • JSON的安全性较低;XML比JSON更安全。
  • JSON不支持注释;XML支持注释。
  • JSON仅支持UTF-8编码;XML支持各种编码。
for...in 和 for...of 有什么区别?
  • for...in 循环主要是为了遍历对象
  • for...of 循环可以用来遍历数组类数组对象字符串SetMap 以及 Generator 对象
如何使用js计算一个html页面有多少种标签?
js
new Set([...document.querySelectorAll('*')].map(ele=> ele.tagName))
谈谈对 window.requestAnimationFrame 的理解

window.requestAnimationFrame() 告诉浏览器你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

与 setTimeout 相比,requestAnimationFrame 最大的优势是由系统来决定回调函数的执行时机。requestAnimationFrame 的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

如何实现上拉加载,下拉刷新?

上拉加载:

js
scrollTop + clientHeight >= scrollHeight;

const doc = document;
const docEle = doc.documentElement;

// 浏览器高度
let clientHeight = docEle.clientHeight;
let scrollHeight = doc.body.scrollHeight;
let scrollTop = docEle.scrollTop;

const distance = 50;

if (scrollTop + clientHeight >= scrollHeight - distance) {
  console.log("开始加载数据");
}

下拉刷新:

  • 监听原生 touchstart 事件,记录其初始位置的值,e.touches[0].pageY;
  • 监听原生 touchmove 事件,记录并计算当前滑动的位置值与初始位置值的差值,大于 0 表示向下拉动,并借助 CSS3 的 translateY 属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;
  • 监听原生 touchend 事件,若此时元素滑动达到最大值,则触发 callback,同时将 translateY 重设为 0,元素回到初始位置
如何判断页面是通过PC端还是移动端访问?
  • navigator.userAgent
js
if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) {
  // 当前设备是移动设备
}
 
// 另一种写法
if (
  navigator.userAgent.match(/Mobi/i) ||
  navigator.userAgent.match(/Android/i) ||
  navigator.userAgent.match(/iPhone/i)
) {
  // 当前设备是移动设备
}
  • window.screen,window.innerWidth

  • window.matchMedia()

js
let isMobile = window.matchMedia("only screen and (max-width: 760px)").matches;

Powered by VitePress.