huruji / blog

红日初升,其道大光:sun_with_face::house_with_garden:请star或watch,不要fork
https://juejin.im/user/5894886f2f301e00693a3e49/posts
158 stars 11 forks source link

JS中那些骚气的小技巧or代码片段 #48

Open huruji opened 6 years ago

huruji commented 6 years ago

1.实现千分位分隔符

使用正则表达式

function addComma(num) {
    return (num + '').replace(/\B(?=(\d{3})+$)/g, ',');
}

addComma(1234)  // 1,234

addComma(12345)  // 12,345
function addComma(num) {
    return (num + '').replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,')
}

addComma(1234)  // 1,234

addComma(12345)  // 12,345
function addComma(num){
        return (num +'').replace(/(?!^)(?=(\d{3})+$)/g,',');
}

addComma(1234)  // 1,234

addComma(12345)  // 12,345

这里涉及到了\B匹配非单词边界、零宽正向先行断言、零宽负向先行断言,参考:正则表达式的先行断言(lookahead)和后行断言(lookbehind)

image

使用 toLocaleString 方法

const addComma = (num) => num.toLocaleString()

addComma(1234)  // 1,234

addComma(12345)  // 12,345

2.精确到指定位数的小数

使用科学计数法

const round = (n, d=0) => Number(`${Math.round(`${n}e${d}`)}e-${d}`)

round(1.2345, 2)    // 1.23

round(1.2345, 3)   // 1.235

3.统计数组中同项出现的次数

const count = (arr) => arr.reduce((obj, name) => {
    obj[name] = obj[name] ?  ++obj[name] : 1;
    return obj;
}, {});

count([1,2,3,4,1,1,2,5])       // { 1:3, 2:2, 3:1, 4:1, 5:1 }

4.使用解构赋值删除不必要的对象属性

let {inter, inter1, ...need} = {inter: 'inter', inter1: 'inter1', a: 'a', b: 'b'};

need  // { a: 'a', b: 'b' }
huruji commented 6 years ago

5.判断是否为纯净的javascript对象

const isPlainObject = (obj) => (
    obj && typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype
);

isPlainObject({})    // true

isPlainObject(new Date())   // false

6.位运算取整

这是最快的取整方法,位运算取整需要注意的是只是取的整数,如果需要和 Math.floor 结果一致,负数应该再减一

取反操作

const floor = (num) => ((num >= 0) ?  ~~num : (~~num) - 1)

左移运算符

const floor = (num) => (num >= 0) ? (num << 0) : ((num << 0) - 1)

右移运算符

const floor = (num) => (num >= 0) ? (num >> 0) : ((num >> 0) - 1)

异或运算符

const floor = (num) => (num >= 0) ? (num^0) : ((num^0) - 1)

7.使用右移运算符取得整除2的值

右移一位即可


5>>1      // 2

6>>1    // 3

8.将一个字符串重复N次

核心就是使用数组的join方法

方法一

const repeat = (target, n) => (new Array(n + 1)).join(target)

方法二,创建类数组对象

const repeat = (target, n) => (
    Array.prototype.join.call({
        length: n + 1
    }, target)
)

方法三,利用闭包缓存方法与对象

const repeat = (() => {
    const join = Array.prototype.join, obj = {};

    return (target, n) => {
        obj.length = n + 1;
        return join.call(obj, target);
    }
})()

方法四,使用数组的concat方法

function repeat(target, n) {
    return (n <= 0) ? '' : target.concat(repeat(target, --n));
}

方法五,利用算法提高效率,使用二分法

const repeat = (target, n) => {
    let s = target, total = '';
    while(n > 0) {
        if(n % 2 == 1) total += s;
        if(n == 1) break;
        s += s;
        n = n >> 1;
    }
    return total;
}
huruji commented 6 years ago

9.使用void操作符得到undefined值

相对于直接使用 undefined 可以节省几个字符

例如检查一个值是否是 undefined

let arg = target === void(0) ? 'a' : 'b'

10.将字符串转换为驼峰风格

const camelize = (target) => {
    if(target.indexOf('-') < 0 && target.indexOf('_') < 0) return target;
    return target.replace(/[-_][^-_]/g, (match) =>(
        match.charAt(1).toUpperCase() ))
}

camelize('my-name')              // myName

11.将字符串转换为下划线风格

const underscored = (target) => (
    target.replace(/([a-z\d])([A-Z])/g, '$1_$2').replace(/\-/g, '_').toLowerCase()
)

12.移除字符串中的html标签

const stripTags = (target) => {
    return String(target || '').replace(/<[^>]+>/g, '');
}

在实际运用中,我们需要考虑 <script> 标签,不应该让 script 标签内的脚本显示出来,因此需要一个函数删除 script 标签内的内容

const stripScript = (target) => (
    String(target || '').replace(/<script[^>]*>[\s\S]*?<\/script>/img, '')
)
huruji commented 6 years ago

13.将字符串HTML转义

const escapeHTML = (target) => (
    target.replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
)

14.数组前导置0

最常见的就是月份和天前面置0

思路一:使用0作为数组的join的方法参数组合之后再截取字符串

const pad = (target, n) => {
    const zero = new Array(n).join('0');
    let str = zero + target;
    return str.substr(-n);
}

pad(12, 4)    /// 0012

可以将new数组的个数先计算出来,这样就省去了截取字符串的一步

const pad = (target, n) => (
    new Array((n + 1) - target.toString().split('').length).join('0') + target
)

pad(12, 4)    /// 0012

思路二:先创建一个含有n个零的大数,再截取

创建这个大数可以使用 Math.pow 、使用左移运算符 << 、或者使用科学计数法。

function pad(target, n) {
    return (Math.pow(10, n) + '' + target).slice(-n);
}
function pad(target, n) {
    return ((1 << n).toString(2) + target).slice(-n)
}
function pad(target, n) {
    return (1e20 + '' + target).slice(-n)
}

思路三:创建拥有N个0的小数,用toFixed方法就行

function pad(target, n) {
    return (0..toFixed(n) + target).slice(-n)
}

(0).toFixed(3)     // 0.000

0..toFixed(2)     // 0.00   注意有两个点,不然报错

15.创建类似与C语言的 printf 方法来格式化字符串

function format(str, object) {
    let arr = [].slice.call(arguments, 1);
    return str.replace(/\\?#\{([^{}]+)\}/gm, (match, name) => {
        if(match.charAt(0) == '\\') return match.slice(1);
        let index = +name;
        if(index >= 0) return arr[index];
        if(object && object[name] !== void(0)) return object[name];
        return '';
    })
}

format('#{0} is a #{1}', 'huruji', 'boy')       // huruji is a boy

format('#{name} is a #{sex}', {name: 'huruji', sex: 'boy'})        //  huruji is a boy

16.原生实现数组的 indexOf 方法

Array.prototype.indexOf = function(item, index) {
    let n = item.length, i = ~~index;
    if(i < 0) i += n;
    for(; i < n; i++) 
        if(this[i] === item) return i;

    return -1
}

这段代码第一个神奇的地方在于使用了双否 ~~运算将其他类型的参数转换为0,先使用Number方法转换得到再使用 否运算 ,而~NaN === -1

第二个神奇的地方在于使用 i += n 实现负数从数组尾部算起的功能

huruji commented 6 years ago

17.使用 ~i 代替 i > -1

我们有时需要判断数组、字符串存在某个元素时,进行某项操作:

if(arr.indexOf(item) > -1) {
    // some code
}

这时候可以使用否操作 ~ 来装X

if(~arr.indexOf(item)) {
    // some code
}

18.柯里化 bindapplycall

出自 JavaScript 框架设计,非常骚气的黑魔法

const bind = function(bind) {
    return {
        bind: bind.bind(bind),
        call: bind.bind(bind.call),
        apply: bind.bind(bind.apply)
    }
}(Function.prototype.bind)

使用

const concat = bind.apply([].concat);
concat([1,2,3], [4,5]);   //    [1, 2, 3, 4, 5]
//等价于
[].concat.apply([1,2,3], [4, 5]);

const concat1 = bind.call([].concat);
concat1([1,2,3], 4, 5, 6)   // [1, 2, 3, 4, 5, 6]
//等价于
[].concat.call([1,2,3], 4, 5, 6);

const concat2 = bind.bind([].concat);
concat2.bind([1,2,3], 14, 15)();

//等价于
[].concat.bind([1,2,3])(14, 15);

用一张简单的代码图表示其中的原理: image

柯里化之后给了我们多一次传参的机会,这样就可通过判断返回不同的结果了。

19.最简单的柯里化函数

1.支持每次只传一个参数的 正宗柯里化函数

function curry(fn) {
    function inner(len, args) {
        if(len <= 0) return fn.apply(null, args);

        return function(x) {
            return inner(len - 1, args.concat(x))
        }
    }

    return inner(fn.length, []);
}

使用:

function sum(x, y, z){
    return x + y + z;
}

curry(sum)(12)(13)(14)     // 39

2.支持一次传多个参数的 简化柯里化函数

function curry(fn) {

    function inner(len, args) {
        if(len <=0) return fn.apply(null, args);

        return function() {
            return inner(len - arguments.length, args.concat([].slice.apply(arguments)))
        }
    }
    return inner(fn.length, []);
}

使用

curry(sum)(12, 13)()(14)    // 39

20.最简单的partial技术实现

与柯里化相似,柯里化缺点在于每次参数都是通过push的,但有些时候有些参数是已经拥有的,我们需要做的是填充没有的参数,这个时候我们可以使用占位操作来代表需要填充的参数,如undefined,不过推荐使用 Object.create(null) ,因为纯空对象没有原型,因此没有toString、valueOf等一系列继承自Object的方法。

这种技术和柯里化技术很适合在延迟调用的场景中使用。

var _ = Object.create(null);

function partial(fn) {
    var args = Array.prototype.slice.call(arguments, 1);

    return args.length < 1 ? fn : function() {
        var innerArgs = [].slice.call(arguments);

        args.forEach((e,i) => {
            if(e === _) args[i] = innerArgs.shift();
        });
        return fn.apply(null, args)
    }
}

使用

function sum() {
    return Array.prototype.slice.call(arguments).reduce((total, e)=> total + e, 0)
}

const c = partial(sum, 1, 2, _, 4, _);

c(3, 5)   // 15
huruji commented 6 years ago

21.Safari Date对象返回NaN

Safari使用 new Date('2018-08-09 09:12') 返回 NaN,原因是safari的Date对象不支持这种格式,可以将 - 转换为 / 即可兼容所有浏览器

typeof data === ‘string’ && new Date(data.replace('-', '/'))

22.判断是否是XML文档

方法一:

const isXML = function(elem) {
    const documentElement = elem && (elem.ownerDocument || elem).documentElement;
    return documentElement ? documentElement.nodeName !== 'HTML' : false;
}

XML相对于HTML来说,XML没有className、getElementById,并且NodeName是区分大小写的。

通过获取文档的顶层文档对象(在html文档中就是HTML节点,判断nodeName是否为HTML),关于属性 ownerDocument 可在这里查阅

方法二:

以上是sizzle的实现,但是事实上,XML的节点是有可能是HTML标签的(虽然这种情况极其少),moottools的slick的实现是通过大量属性判断的:

const isXML = function(document) {
    return (!!document.xmlVersion) || 
            (!!document.xml) || 
            (Object.prototype.toString.call(document) == '[object XMLDocument]') || 
            (document.nodeType == 9 && document.documentElement.nodeName != 'HTML')
}

方法三:

标准浏览器中暴露了HTML文档的构造器HTMLDocument,而IE下的XML元素有selectNodes属性,因此可以判断是否是HTMLDocument的实例以及是否拥有selectNodes属性。

const isXML = window.HTMLDocument ? function(doc) {
    return !(doc instanceof HTMLDocument);
} : function(doc) {
    return "selecNodes" in doc;
}

方法四: 面对属性,其实使用JavaScript就可以随意添加属性,因此属性判断方法很容易被攻破,因此功能法才会更加有效,XML和HTML都支持createElement方法,但是XML区分大小写,因此可以创建两个大小写元素,判断是否相等即可,这是比较严谨的方法:

const isXML = function(doc) {
    return doc.createElement('p').nodeName !== doc.createElement('P').nodeName;
}

23.判断节点是否包含另一个节点

const contains = function(a, b, same) {
    // same属性,是否可以是同一个node
    if(a === b) return !!same;
    if(!b.parentNode) return false;
    if(a.contains) {
        return a.contains(b);
    } else if(a.compareDocumentPosition) {
        return !!(a.compareDocumentPosition(b) & 16);
    }
    while((b= b.parentNode))
        if(a === b) return true;
    return false;
}

contains 是IE的私有属性,现在chrome、firfox也有这个方法。compareDocumentPosition 方法是判断两个节点的位置,具体有以下: 有时候两个元素的位置可能满足上表的两种情况,如返回20,因此判断中使用了与运算来判断是否包含这个情况,我们需要注意到所有的结果都是2的倍数,因此与运算可以保证这点。

关于 compareDocumentPosition

24.判断两个节点的顺序

两个节点相同返回0,a节点在前返回-1,b节点在前返回1

const sortNodes = function(a, b) {
    var p = "parentNode",
        ap = a[p],
        bp = b[p];

    if(a === b) {
        return 0;
    } else if(ap == bp) {
        while(a = a.nextSibling) {
            if(a === b) return -1
        }
        return 1;
    } else if(!ap) {
        return -1;
    } else if(!bp) {
        return 1;
    }

    var al = [], ap = a;
    while(ap && ap.nodeType === 1) {
        al[al.length] = ap;
        ap = ap[p];
    }
    var bl = [], bp = b;
    while(bp && bp.nodeType === 1) {
        bl[bl.length] = bp;
        bp = bp[p];
    }
    ap = al.pop();
    bp = bl.pop();
    while(ap === bp) {
        ap = al.pop();
        bp = bl.pop();
    }
    if(ap && bp) {
        while(ap = ap.nextSibling) {
            if(ap === bp) return -1;
        }
        return 1;
    }
    return ap ? 1: -1;
}

思路是:先判断两个节点是否相同,判断两个节点的父节点是否相同(相同则使用nextSibling属性),这里需要注意的是,如果节点就是根节点,就没有父节点(这个估计很多人会漏掉),最后也是最核心的方法就是不断向上取父元素,直到根元素,之后不断pop出来,判断是否父元素相等。(最最核心)。

很明显,上面的整个函数可以作为数组sort方法的参数,只要将取得的元素节点数组化就行了。

var eles = [].slice.call(document.getElementsByTagName('*'))
            .sort(sortNodes)

24.chrome 66之后video与audio标签autoplay属性失效

一定需要自动播放时,可以添加静音muted属性即可

<video src="./test.mp4" width="600" height="400" controls autoplay muted></video>

ref: Chrome 66禁止声音自动播放之后

huruji commented 5 years ago

25. Promise 化 callback 调用

将 callback 的调用转化为 Promise 调用

const promisify = (fn) => (...args) => new Promise((resolve, reject) => {
        args.push(resolve)
        fn.apply(this, args)
    })

测试

const testCb = function (a, b, callback) {
    setTimeout(() => {
        callback(a + b)
    }, 1000)
}

promisify(testCb)(10, 12)
    .then(data => {
        console.log(data)
    })