常用函数封装 | 青训营笔记

143 阅读6分钟

这是我参与第四届青训营笔记创作活动的第十五天

最近的项目基本完工,在做项目的过程中发现了一些好玩的函数,请允许我在这里整理一些!事不宜迟,我们开始吧。

深度拷贝

深拷贝就是相对与浅拷贝而言的,最主要的差异体现在引用类型上,从本质上讲就是浅拷贝只简简单单地把栈当中的引用地址拷贝了一份,所以当你修改新拷贝出来的值的时候,被拷贝的对象也会被你修改掉;而深拷贝是会在堆内存当中为新对象建立空间,所以被拷贝的对象就不会被无缘无故地被修改掉了,牵一发而动全身,这不是我们需要的,所以我们选择了深度拷贝。

深度拷贝的实现方法

其实深度拷贝的实现方法还蛮多的,不过我们可以对比分析一下。

使用“Object.assign({},obj)”语句来实现

Object.assign默认是对对象进行深拷贝的,但是我们需要注意的是,它只对最外层的进行深拷贝,也就是当对象内嵌套有对象的时候,被嵌套的对象进行的还是浅拷贝;

   function cloneDeepAssign(obj){
       return Object.assign({},obj)
   }

注意:数组拷贝方法当中,使用...sliceconcat等进行拷贝也是一样的效果,只深拷贝最外层。同时,我们知道Object.assign针对的是对象自身可枚举的属性,对于不可枚举的没有效果,也不支持undefined。

所以,当我们对于一个层次单一对象的时候,才可以考虑这种方法,简单快捷。

利用JSON

这可能是大家最常提到的一种深拷贝的方式,一般大部分的深拷贝都可以用JSON的方式进行解决,本质是因为JSON会自己去构建新的内存来存放新对象。

function cloneDeepJson(obj){
    return JSON.parse(JSON.stringfy(obj))
}

但是我们要注意的是:

  • 会忽略 undefinedsymbol
  • 不可以对Function进行拷贝,因为JSON格式字符串不支持Function,在序列化的时候会自动删除;
  • 诸如 MapSetRegExpDateArrayBuffer 和其他内置类型在进行序列化时会丢失;
  • 不支持循环引用对象的拷贝;(循环引用的可以大概地理解为一个对象里面的某一个属性的值是它自己)

利用递归循环

我们直接上代码,其实就是判断当前target的类型,如果是对象就递归循环,直到对象全部拷贝完毕。

 function deepCopy(target) {
    if (typeof target == 'object') {
        const result = Array.isArray(target) ? [] : {}
        for (const key in target) {
            if (typeof target[key] == 'object') {
                result[key] = deepCopy(target[key])
            } else {
                result[key] = target[key]
            }
        }

        return result
    }

    return target
}

实现水印功能

有些时候由于项目需要实现水印功能,我们就不得不说一下水印的功能实现了。不过其实实现水印功能的方法也挺多的,让我们一起学习一下。

基于原图生成水印图片

这种方案就是将 原图片 添加水印之后生成了 新图片,后续在前端页面进行展示是后端接口不返回原图片,而是返回带有水印的图片即可。这种方式最大的优点就是安全,因为 水印图片 是后端生成的,前端只需要负责展示即可,不需考虑多余的问题,且即便在前端页面保存对应图片,拿到的仍然不是原图片。

基于 DOM 实现水印效果

这个的前提其实是已经有了水印的背景图片,而前端要做的就是创建一个waterMark的DOM节点,比如用于包裹对应的img便于展示水印内容,随后再创建另一个waterbg的DOM节点作为img的兄弟节点,用来放置水印,随后用绝对定位保证waterbg在最上层,且设置点击穿透,即pointer-events:none;将对应的水印图片作为waterbg的背景图片。

代码后续补上。。。

基于 Canvas 实现水印效果

基于 Canvas 实现方式的优点就在于能够动态的设置水印内容,相比于上一种基于固定背景图片的方式更灵活。 通过 canvas 填充文本,并通过 canvas.toDataURL("image/png"); 获取到对应的 base64 格式的图片后将这个 base64 格式的图片作为类名为 water-mark 节点的背景图。利用 background-repeat: repeat; 让这个图重复填充背景即可,然后为 water-mark 节点设置 pointer-events: none; 实现 点击穿透。最后利用对应图片的父元素作为 water-mark 节点的相对定位节点,保证绝对定位的 water-mark 节点显式在对应图片之上。

代码如下:

 // 全局保存canvas和div,避免重复创建
    const globalCanvas=null;
    const globalWaterMark=null;

    // 获取toDataUrl的结果
    function getDataUrl(
        font = "20px normal",
        fillStyle="rgba(0,0,0,1)",
        textAlign,
        textBaseline,
        text="我是水印",
    ){
        const rotate=-20;
        const canvas=globalCanvas||document.createElement("canvas");
        const ctx=canvas.getContext("2d");//获取画布上下文
        ctx.rotate((rotate*Math.PI)/180);
        ctx.font=font;
        ctx.fillStyle=fillStyle;
        ctx.textAlign=textAlign||"left";
        ctx.textBaseline=textBaseline||'middle';
        ctx.fillText(text,canvas.width/8,canvas.height/2);
        return canvas.toDataURL("image/png");
    }
   
    // 设置水印
    function setWaterMark(el){
        // 获取对应的canvas画布相关的base64URL
        const url=getDataUrl();

        // 创建waterMark父元素
        const waterMark=globalWaterMark|| document.createElement("div");
        waterMark.className=`water-mark`;
        waterMark.setAttribute("style",`background-image:url(${url});`);

        // 将对应图片的父容器作为定位元素
        el.setAttribute("style","position:relative;");
        // 将图片元素移动到waterMark中
        el.appendChild(waterMark);
        return `background-image:url(${url});`;
    }

    // 监听DOM变化
    function creatObserver(el){
        const waterMarkEl=el.querySelector(".water-mark");
        const observer=new MutationObserver((mutationsList)=>{
            if(mutationsList.length){
                const {removedNodes,type,target}=mutationsList[0];
                const currStyle=waterMarkEl.getAttribute("style");
                // 证明被删除了
                if(removedNodes[0]==waterMarkEl){
                    observer.disconnect();
                    init(el);
                }else if(type==="attributes"&&target===waterMarkEl&&currStyle!==style){
                    waterMarkEl.setAttribute("style",style);
                }
            }    
        });
        observer.observe(el,{
            childList:true,
            attributes:true,
            subtree:true,
        })
    }
    function init(el){
        // 设置水印与启动监控同时开启
        creatObserver(el,style=setWaterMark(el));
    }

     const el=document.querySelector('.container');
     init(el);

image.png

而且就算打开控制台把base64删掉也没用,一旦被监听到就会恢复。

字符串转文件

有时候我们需要将字符串转化成json文件,或者txt文件,抑或是markdown文件,这个时候就可以用到这个函数。不过前提是你原先的字符串要对应生成文件的某些格式。举个例子,假如要生成markdown文件中的标题效果,字符串中必须有md代码#。

function generateFile(str,type){
        let stringData=str;
        let fileType=type;
        if(type=='json'){
            fileType='application/json';
            stringData=JSON.stringify(str);
        }else if(type=='txt'){
            fileType="text/plain;charset=utf-8";
        }else if(type=='md'){
            fileType='text/markdown';
        }
        // dada 表示要转换的字符串数据,type 表示要转换的数据格式
        const blob = new Blob([stringData], {
            type: fileType
        })
         // 根据 blob生成 url链接
        const objectURL = URL.createObjectURL(blob)
        // 创建一个 a 标签Tag
        const aTag = document.createElement('a')
        // 设置文件的下载地址
        aTag.href = objectURL
        // 设置保存后的文件名称
        aTag.download = "json文件.json"
        aTag.download = `${type}文件.${type}`
        // 给 a 标签添加点击事件
        aTag.click()
        // 释放一个之前已经存在的、通过调用 URL.createObjectURL() 创建的 URL 对象。
        // 当你结束使用某个 URL 对象之后,应该通过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件的引用了。
        URL.revokeObjectURL(objectURL)
    }

调用函数时候,只需要传入文件类型以及字符串就可以了。

好了,今天的函数就先到这里。最后的最后,谢谢大家这么厉害还来看我,如果发现问题或者需要补充的点麻烦大家通过评论告诉我。博取众长,共同进步!