分类: JavaScript

  • 离开页面时调接口

    场景:当前页面正在执行异步任务,当用户点击导航栏或直接关闭页面时,需调接口取消任务,减少资源浪费。

    问题:如果在beforeunload事件中调ajax 请求,则会出现浏览器cancel掉请求的情况。

    解决:使用sendBeacon方法,适用于发送少量数据到服务器。

    Navigator.sendBeacon()

    这个方法主要用于满足统计和诊断代码的需要,这些代码通常尝试在卸载(unload)文档之前向 Web 服务器发送数据。过早的发送数据可能导致错过收集数据的机会。然而,对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在 unload 事件处理器中产生的异步 XMLHttpRequest

    Vue 3代码如下

    <script setup>
    import { onBeforeUnmount, onMounted } from 'vue';
    
    // 取消任务的API函数
    function cancelTask() {
      navigator.sendBeacon('/api/cancel-task', JSON.stringify({ task: 'taskId' }));
    }
    
    onMounted(() => {
      // 添加 beforeunload 事件监听器
      window.addEventListener('beforeunload', cancelTask);
    });
    
    onBeforeUnmount(() => {
      // 组件卸载前移除监听器
      window.removeEventListener('beforeunload', cancelTask);
    });
    </script>
    
  • parseInt

    parseInt永远将输入解析为字符串

    parseInt({toString: () => 2, valueOf: () => 1}); // -> 2
    Number({toString: () => 2, valueOf: () => 1}); // -> 1
    

    一个常见的栗子

    parseInt("apple"); // -> NaN
    parseInt("apple", 16); // -> 10
    

    上面这个例子是因为parseInt会一个个解析字符直到解析不了,所以解析十六进制中的a为10作为结果返回。

    parseInt Infinity

    parseInt("Infinity", 10); // -> NaN
    // ...
    parseInt("Infinity", 18); // -> NaN...
    parseInt("Infinity", 19); // -> 18
    // ...
    parseInt("Infinity", 23); // -> 18...
    parseInt("Infinity", 24); // -> 151176378
    // ...
    parseInt("Infinity", 29); // -> 385849803
    parseInt("Infinity", 30); // -> 13693557269
    // ...
    parseInt("Infinity", 34); // -> 28872273981
    parseInt("Infinity", 35); // -> 1201203301724
    parseInt("Infinity", 36); // -> 1461559270678...
    parseInt("Infinity", 37); // -> NaN
    

    parseInt null

    parseInt(null, 24); // -> 23
    

    上面的结果都是一样的解析过程,按字母表排序,每个字符对应数字是固定的。比如,I对应18,在18进制中只能解析0-17,所以parseInt("Infinity", 18); // -> NaN,在19进制中就能解析出结果:parseInt("Infinity", 19); // -> 18

    n对应23,在23进制中解析不了,只返回18,在24进制中就是18 * (24 ^ 5) + 23 * (24 ^ 4)+ 15 * (24 ^ 3)+ 18 * (24 ^ 2) + 23 * 24 + 18 = 151176378,其他依此类推。

    八进制

    parseInt("08"); // -> 8, 支持ES5
    parseInt("08"); // -> 0, 不支持ES5
    

    以0开头的数字可以被解析为10进制或8进制,ECMAScript 5规定为10进制,但不是所有浏览器都支持它。所以使用parseInt时指定进制是好习惯,有时可以避免不必要的bug。

    浮点数

    parseInt(0.000001); // -> 0
    parseInt(0.0000001); // -> 1
    parseInt(1/1999999); // -> 5
    

    parseInt 接受一个字符串参数并返回一个指定基数的整数。 parseInt 还会去除字符串参数中第一个非数字之后的任何内容。0.000001转换为字符串后是"0.000001",0.0000001转换后是"1e-7",所以返回1,1/1999999转换后是5.00000250000125e-7,返回结果是5。

  • async/await class constructor

    背景:期望在新建类时调后台接口对类进行配置,配置成功后才可以执行其他操作。

    尝试封装类的构造函数为async/await,报错

    Class constructor may not be an async method

    原因:async函数返回值为promise,而构造函数返回值为object,同一个函数不可能返回值同时为promiseobject

    解决:参考 jQuery's ready()方法

    1. 类实现

        class myClass {
            constructor () {
    
            }
    
            async ready (callback) {
                await yourHttpRequest();
                // 回调函数绑定作用域
                callback.bind(this)();
            }
        }

    2. 调用

        const myObj = new myClass();
        myObj.ready(function() {
            // 保证配置完才能执行其他操作
        });
    

    参考:https://stackoverflow.com/questions/43431550/async-await-class-constructor

  • append vs appendChild

    append和appendChild都用于在DOM中添加新的元素,区别如下:

    1. append传参可以为DOMString和Node节点,appendChild传参只能为Node节点
    // 1. append传Node节点
    const parent = document.createElement('div');
    const child = document.createElement('p');
    parent.append(child);
    
    // 2. append传文本,被插入的 DOMString对象等价为 Text 节点
    const parent = document.createElement('div');
    parent.append('Appending Text');
    
    // 3. appendChild传Node节点
    const parent = document.createElement('div');
    const child = document.createElement('p');
    parent.appendChild(child);
    
    // 4. 报错:appendChild传文本
    const parent = document.createElement('div');
    parent.appendChild('Appending Text');
    appendChild不支持传字符串

    2. append没有返回值,appendChild返回被创建的Node节点

    const parent = document.createElement('div');
    const child = document.createElement('p');
    const appendValue = parent.append(child);
    console.log(appendValue) // undefined
    
    const appendChildValue = parent.appendChild(child);
    console.log(appendChildValue) // <p><p>
    

    3. append可以同时添加多个元素,appendChild同时只能添加一个

    const parent = document.createElement('div');
    const child = document.createElement('p');
    const childTwo = document.createElement('p');
    parent.append(child, childTwo, 'Hello world'); // Works fine
    
    parent.appendChild(child, childTwo, 'Hello world');
    // Works fine, but adds the first element and ignores the rest
    

    4. 浏览器兼容区别,appendChild在各浏览器中兼容性较好

    其他:DOMString是一个UTF-16字符串。由于JavaScript已经使用了这样的字符串,所以DOMString 直接映射到 一个String

  • Navigator Clipboard 复制不生效

    使用 navigator.clipboard.writeText 完成复制功能的实现时,在本地测试没有问题,部署后报错navigator.clipboard Cannot read property ‘writeText‘ of undefined

    原因:Navigator API 的安全策略禁用了非安全域的 navigator.clipboard 对象,API 仅支持通过 HTTPS 提供的页面。为帮助防止滥用,仅当页面是活动选项卡时才允许访问剪贴板。活动选项卡中的页面无需请求许可即可写入剪贴板,但从剪贴板读取始终需要许可。

    https://w3c.github.io/clipboard-apis/#dom-navigator-clipboard

    解决:判断当前环境是否支持navigator clipboard API,不允许则使用 document.execCommand('copy')进行剪贴板交互。
    使用兼容方案原因:navigator clipboard API是异步API,而使用document.execCommand('copy')进行剪贴板访问是同步的,只能读写 DOM,效率低下且在各浏览器之间还可能存在不同,在支持navigator clipboard API的情况下应尽量避免使用document.execCommand('copy')

    传入DOM ID

    export function copyCurrentTarget(text, id = '') {
        if (navigator.clipboard && window.isSecureContext) {
            navigator.clipboard.writeText(text)
        } else {
            window.getSelection().removeAllRanges()
            const questionToCopy = document.querySelector('#' + id)
            const range = document.createRange()
            range.selectNode(questionToCopy)
            window.getSelection().addRange(range)
            try {
                const successful = document.execCommand('copy')
                if (successful) {
                    console.log('复制成功')
                } 
            } catch (error) {
                console.error(error)
            }
        }
    }
    

    视口外创建一个新的DOM,传入要复制的内容

    function copyToClipboard(textToCopy) {
        if (navigator.clipboard && window.isSecureContext) {
            return navigator.clipboard.writeText(textToCopy)
        } else {
            let textArea = document.createElement("textarea")
            textArea.value = textToCopy
            textArea.style.position = "fixed"
            textArea.style.left = "-999999px"
            textArea.style.top = "-999999px"
            document.body.appendChild(textArea)
            textArea.focus()
            textArea.select()
            return new Promise((res, rej) => {
                document.execCommand('copy') ? res() : rej()
                textArea.remove()
            })
        }
    }
    

    扩展:安全和权限

    复制和粘贴权限已添加到 Permissions API 中。当页面处于活动标签页时,会自动授予 clipboard-write 权限。 clipboard-read 权限必须手动请求。如果尚未授予权限,尝试读取或写入剪贴板数据的操作会自动提示用户授予权限。

    const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
    const permissionStatus = await navigator.permissions.query(queryOpts);
    // 'granted', 'denied' or 'prompt':
    console.log(permissionStatus.state);
    
    permissionStatus.onchange = () => {
      console.log(permissionStatus.state);
    };

    因为 Chrome 仅在页面是活动选项卡时才允许剪贴板访问,某些示例如果直接粘贴到 DevTools 中将无法运行,因为 DevTools 本身就是活动选项卡。有一个技巧:使用 setTimeout() 延迟剪贴板访问,然后在调用函数之前快速点击页面内部以将其聚焦:

    setTimeout(async () => {
    const text = await navigator.clipboard.readText();
    console.log(text);
    }, 2000);

    要在 iframe 中使用 API,需要使用权限策略启用它

    <iframe
        src="index.html"
        allow="clipboard-read; clipboard-write"
    >
    </iframe>

    参考:https://stackoverflow.com/questions/51805395/navigator-clipboard-is-undefined、https://developer.chrome.com/blog/cut-and-copy-commands/、https://web.dev/async-clipboard/

  • 判断对象为空

    ❌ 错误做法:

    let obj = {}
    obj == {} // false

    ✔ 正确做法:

    const isObjEmpty = (obj) => !Reflect.ownKeys(obj).length && obj.constructor === Object;

    注意:

    • 数组也可以使用Reflect.ownKeys方法,所以需要判断类型
    • 为什么不用Object.keys()Object.keys只能返回对象中可枚举的属性,Reflect.ownKeys返回所有属性
    var obj = {
        a: 1,
        b: 2
    }
    Object.defineProperty(obj, 'method', {
        value: function () {
        alert("Non enumerable property")
    },
        enumerable: false
    })
    ​
    console.log(Object.keys(obj))
    // ["a", "b"]
    console.log(Reflect.ownKeys(obj))
    // ["a", "b", "method"]