震惊,JavaScript竟然还有这些方法

2,379 阅读9分钟

概述

在JavaScript,DOM,BOM中有一些比较冷门但是非常好用的方法,我在这里称之为高级方法,这些方法没有被广泛使用或多或少是因为存在一些兼容性的问题,不是所有的浏览器都读得懂的。
这篇文章主要就是对这些方法做一个总结,同时提供一些DEMO来加深这些方法有哪些使用场景,同时希望能在我们开发过程提供一些帮助,一起来看一下吧。

详细

elementFromPoint和elementsFromPoint

兼容性:

image.png

具体使用方法:

document.elementFromPoint 和 document.elementsFromPoint (注意是 elements,多个“s”)。document.elementFromPoint 获取指定坐标点最上面的元素,而 document.elementsFromPoint 获取指定坐标点下面的所有元素,包括 html 元素,body 元素取决于其是否在指定的坐标上。
很多白屏检测库,会用到这些方法。

方法参数和返回:

  • elementFromPoint(x: number, y: number): Element
  • elementsFromPoint(x: number, y: number): Array

注意:

  • point-events 设置为 none 的元素将被忽略。
  • 如果指定的坐标点在文档的可视范围外,或者坐标包含负数,那么结果返回 null 或 [] (与浏览器版本有关)。
  • 如果指定的位置为匿名元素(如 ::before, ::after)或 滚动条,则返回其最近一个非匿名类型的祖先元素。

使用场景

  • 白屏检测:通过查找指定位置的DOM元素,判断起是否存在来判断页面状态,通过document.elementsFromPoint在我们水平上取20个点,竖直上取20个点判断,组成十字架判断白屏方法,如果eleemntPoint数目小于一定值,则报白屏。DEMO白屏DEMO非白屏
/**
 * 检查页面白屏,横向,纵向18个点, > 17/18就认为白屏上报
 */

FEDLOG.blank = function () {
  if (!document.elementsFromPoint) {
    return;
  }
  const wrapperCls = ['body', 'html']
  let nothingCnt = 0
  let totalCnt = 0
  const getSel = (el) => {
    if (!el) return ''
    return (el.classList && el.classList[0]) || el.id || el.localName
  }
  const isWrap = (el) => {
    if (!el) return;
    totalCnt++
    if (wrapperCls.indexOf(getSel(el)) >= 0) {
      nothingCnt++
    }
  }
  let elementsX, elementsY;
  for (let i = 1; i < 10; i++) {
    elementsX = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2)
    elementsY = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10)
    isWrap(elementsX[0])
    isWrap(elementsY[0])
  }
  if (totalCnt - nothingCnt < 2 && !this._sendBlank) {
    let centerEl = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 2)

    this._sendBlank = true
  }
};

MutationObserver

兼容性:

image.png

具体使用方法:

MutationObserver API是一个JavaScript API,用于监视DOM树的变化。它可以在DOM树中的某个节点发生变化时被通知,并且可以执行相应的操作。

  • 节点插入或删除
  • 属性值变化
  • 文本内容变化
  • 子节点的顺序变化

方法参数和使用:

//选择一个需要观察的节点
var targetNode = document.getElementById('some-id');

// 设置observer的配置选项
var config = { attributes: true, childList: true, subtree: true };

// 当节点发生变化时的需要执行的函数
var callback = function(mutationsList, observer) {
    for(var mutation of mutationsList) {
        if (mutation.type == 'childList') {
            console.log('A child node has been added or removed.');
        }
        else if (mutation.type == 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};

// 创建一个observer示例与回调函数相关联
var observer = new MutationObserver(callback);

//使用配置文件对目标节点进行观测
observer.observe(targetNode, config);

// 停止观测
observer.disconnect();

这个示例代码会监视#some-id节点及其子节点的变化。
配置属性有以下选项:

配置对象,用于指定需要监视的DOM树变化类型。它包含以下属性:

- `childList`:布尔值,表示是否监视目标节点的子节点变化。
- `attributes`:布尔值,表示是否监视目标节点的属性变化。
- `attributeFilter`:一个数组,包含需要监视的属性名称。如果指定了该属性,则只有指定的属性发生变化时才会通知观察者。
- `subtree`:布尔值,表示是否监视目标节点的子孙节点变化。
- `characterData`:布尔值,表示是否监视目标节点的文本内容变化。
- `characterDataOldValue`:布尔值,表示是否在文本内容变化时记录旧的文本内容。

使用场景

  • 实时DOM级别录屏:客户端采用MutationObserver监听,每当变化时,将数据同步到服务端,进行过滤和处理,再同步给观察端,就可以看到客户端的所有dom操作。DEMO
  • **DOM防篡改:**防止页面上高危DOM被认为修改,例如DOM实现的水印,当用户尝试把水印移除时,通过MutationObserver监听监听到这个行为,再强制把水印设置上去。DEMO

IntersectionObserver

兼容性:

image.png

具体使用方法:

网页开发时,常常需要了解某个元素是否进入了"视口"(viewport),即用户能不能看到它。
传统的实现方法是,监听到 scroll 事件后,调用目标元素的 getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于 scroll 事件密集发生,计算量很大,容易造成性能问题。
目前有一个新的 IntersectionObserver API,可以自动"观察"元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做"交叉观察器"。

方法参数和使用:

IntersectionObserver接受两个参数:

  • callback:是可见性变化时的回调函数。
  • option:是配置对象(该参数可选)。
const observer = new IntersectionObserver(callback, options);

/*entries
target:观察的目标元素。
intersectionRatio:目标元素与视口的交叉比例,值在 0 到 1 之间。
isIntersecting:目标元素是否与视口相交。
intersectionRect:目标元素与视口的交叉区域的位置和尺寸信息。
*/
const callback = (entries, observer) => {
  // 处理交叉状态变化的回调函数
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 元素进入视口
    } else {
      // 元素离开视口
    }
  });
};

/*options
root:指定观察器的根元素,默认为视口。
rootMargin:设置根元素的外边距,用于扩大或缩小交叉区域。
threshold:指定交叉比例的阈值,可以是单个数值或由多个数值组成的数组。
*/
const options = {
  // 可选配置
};
const target = document.querySelector('#targetElement');
observer.observe(target);

使用场景

  • 图片懒加载:传统懒加载是通过监听 scroll 事件实现的,但是 scroll 事件会在很短的时间内触发很多次,严重影响页面性能,采用IntersectionObserver也可以实现:
const imgs = 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)

imgs.forEach((image) => {
  observer.observe(image)
})
  • 可视区域无限滚动:传统方法一般是监听 scroll, 在回调方案中 手动计算偏移量然后计算定位,由于 scroll事件密集发生,计算量很大,容易造成性能问题。所有的这些计算都是为了判断一个 dom 是否在可视范围内,IntersectionObserver实现虚拟列表方案将大大简化。DEMO

image.png

getComputedStyle

兼容性:

image.png

具体使用方法:

getComputedStyle()是一个JavaScript API,可以获取当前元素所有最终使用的CSS属性值的方法,它返回一个CSSStyleDeclaration对象。
一般情况下,使用style属性来获取元素的内联样式,但在实践中并不常用,因为style并不会返回其他地方的规则,比如class类的样式等,getComputedStyle()不仅可以获取到元素的所有样式,也可以获取到伪类,伪元素等的样式。

方法和参数返回

getComputedStyle() 接受两个参数:

  • element 是指定要返回样式的元素。如果是其它节点类型,例如 Text 节点,方法将会报错。
  • pseudoElement指定要匹配的伪元素。默认为null。
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JS getComputedStyle() Demo</title>
  <style type="text/css">
    .message {
      background-color: #fff3d4;
      border: solid 1px #f6b73c;
      padding: 20px;
      color: black;
    }
  	p::first-letter {
      font-size: 1.5em;
      font-weight: normal
    }
  </style>
</head>
<body>
  <p class="message" style="color:red">
    这是getComputedStyle() Demo!
  </p>
  <p id="wei">伪元素getComputedStyle() Demo</p>
  <script>
    // 常见用法
    let message = document.querySelector('.message');
    let style = getComputedStyle(message);

    console.log('color:', style.color);
    console.log('background color:', style.backgroundColor);
    // 输出:
    // color: rgb(255, 0, 0)
    // background color: rgb(255, 243, 212)

  	// 伪元素用法
		let p = document.getElementById('wei');
    let style = getComputedStyle(p, '::first-letter');
    console.log(style.fontSize);
    // 输出:24px

  </script>
</body>
</html>

使用场景

  • 获取元素所有样式:元素本身,伪类,伪元素。

Proxy

兼容性:

image.png

具体使用方法:

Proxy API对应的Proxy对象是ES2015就已引入的一个原生对象,用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
从字面意思来理解,Proxy对象是目标对象的一个代理器,任何对目标对象的操作(实例化,添加/删除/修改属性等等),都必须通过该代理器。因此我们可以把来自外界的所有操作进行拦截和过滤或者修改等操作。
基于Proxy的这些特性,常用于:

  • 创建一个可“响应式”的对象,例如Vue3.0中的reactive方法。
  • 创建可隔离的JavaScript“沙箱”。

方法参数和使用:

const p = new Proxy(target, handler)
  • target:要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler:以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

下面是一个简单用法:

let foo = {
  a: 1,
  b: 2
}
let handler = {
    get:(obj,key)=>{
        console.log('get')
        return key in obj ? obj[key] : undefined
    }
}
let p = new Proxy(foo,handler)
console.log(p.a) // 1

使用场景

  • 禁止删除和修改对象的某个属性:对于一个代理对象来说,可以通过Proxy禁止代码对其进行修改。
let foo = {
  a:1,
  b:2
}
let handler = {
  set:(obj,key,value,receiver)=>{
    console.log('set')
    if (key == 'a') throw new Error('can not change property:'+key)
    obj[key] = value
    return true
  },
  deleteProperty:(obj,key)=>{
    console.log('delete')
    if (key == 'a') throw new Error('can not delete property:'+key)
    delete obj[key]
    return true
  }
}

let p = new Proxy(foo,handler)

p.a = 3 // Uncaught Error

delete p.a  // Uncaught Error

其他延伸

JavaScript 提供了许多 API,有些可能相对冷门,因为它们在特定场景下才会被用到。以下是一些比较冷门的 JavaScript API:

Battery Status API:

用于获取设备电池状态信息,包括电量百分比、是否正在充电等。

navigator.getBattery().then(function (battery) {
   console.log(battery.level); // 当前电池电量
   console.log(battery.charging); // 是否正在充电
});

### Page Visibility API:

允许检测页面是否在前台可见状态。

document.addEventListener('visibilitychange', function () {
       if (document.hidden) {
         console.log('页面不可见');
       } else {
         console.log('页面可见');
       }
});

Battery Status API:

用于获取设备电池状态信息,包括电量百分比、是否正在充电等。

navigator.getBattery().then(function (battery) {
   console.log(battery.level); // 当前电池电量
   console.log(battery.charging); // 是否正在充电
});

Media Devices API:

用于获取和控制媒体设备,如摄像头和麦克风。

navigator.mediaDevices.enumerateDevices().then(function (devices) {
   devices.forEach(function (device) {
      console.log(device.kind, device.label);
   });
});

Network Information API:

提供有关网络连接的信息,如带宽和连接类型。

const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
console.log(connection.downlink); // 下行速度
console.log(connection.effectiveType); // 有效连接类型

Screen Capture API:

允许通过蓝牙与外围设备进行通信。

 navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
   .then(device => device.gatt.connect())
   .then(server => server.getPrimaryService('heart_rate'))
   .then(service => service.getCharacteristic('heart_rate_measurement'))
   .then(characteristic => characteristic.readValue());

这些 API 在特定的应用场景中可能非常有用,但由于其用途相对特定,可能不太为人所熟知。在实际开发中,根据项目需求,可能会遇到需要使用这些 API 的情况,但是要注意他们的浏览器兼容性。