程序人生 A log of my life

js tips

原型链

js对象都有一个__proto__属性指向它的原型对象(ES6的标准语法是Object.getPrototypeOf()),原型对象也有__proto__,自然形成一个链。当访问对象属性时,js会从对象自己,然后一直顺着原型链找下去,直到找到一个包含属性的对象。__proto__的生成规则:

  • 当直接创建对象时,创建出的对象的__proto__是Object.prototype,直接创建数组时创建出的数组的__proto__是Array.prototype。
  • 当使用new a()创建对象时,创建出的对象的__proto__是a.prototype。
  • 当使用Object.create(a)创建对象时,创建出的对象的__proto__就是a。

js执行方法时也顺着原型链去找,需要注意,即使找到了原型里的方法,this还是”子”对象,举例:

var o = {
  a: 2,
  m: function() {
    return this.a + 1;
  }
};
console.log(o.m()); // 3
var p = Object.create(o);
p.a = 4; 
console.log(p.m()); // 5

Number.MAX_SAFE_INTEGER

这个是js里能保持精度的最大整数,实际上是power(2,53) - 1,正负都不能超过这个数,过了就会计算出错,且没有异常。需要注意的是这个数比java的long要小很多,所以要十分小心,比如java 后台返回json数据里如果有大的整数超过这个范围,在java侧是好的,到js侧就出错了。

promise

  • promise的finally部分是一个比较新的特性,很多浏览器不支持(尤其是移动端),小心使用。

遍历数组

  • 注意forEach有个明显的缺点,不能break和return
  • for in 是用于遍历对象,不要用来遍历数组
  • ES6的for of可以用

lodash

虽然lodash很好用,类似Java里的Apache common,但注意lodash会给js增加30k(gzip之后), 所以通常应该根据需要导入特定的函数,其中合并对象的几个函数很容易混淆:

  • assign和merge用于向第一个object复制额外的属性
  • defaults和defaultsDeep只在第一个object不存在这个属性才复制,所以类似取缺省值的含义。
  • assign和defaults执行浅拷贝,merge和defaultsDeep执行深拷贝,或许merge改名为assignDeep更容易理解。

更多细节如下:

_.assign      ({}, { a: 'a' }, { a: 'bb' }) // => { a: "bb" }
_.merge       ({}, { a: 'a' }, { a: 'bb' }) // => { a: "bb" }
_.defaults    ({}, { a: 'a' }, { a: 'bb' }) // => { a: "a"  }
_.defaultsDeep({}, { a: 'a' }, { a: 'bb' }) // => { a: "a"  }

_.assign handles undefined but the others will skip it

_.assign      ({}, { a: 'a'  }, { a: undefined }) // => { a: undefined }
_.merge       ({}, { a: 'a'  }, { a: undefined }) // => { a: "a" }
_.defaults    ({}, { a: undefined }, { a: 'bb' }) // => { a: "bb" }
_.defaultsDeep({}, { a: undefined }, { a: 'bb' }) // => { a: "bb" }

They all handle null the same

_.assign      ({}, { a: 'a'  }, { a: null }) // => { a: null }
_.merge       ({}, { a: 'a'  }, { a: null }) // => { a: null }
_.defaults    ({}, { a: null }, { a: 'bb' }) // => { a: null }
_.defaultsDeep({}, { a: null }, { a: 'bb' }) // => { a: null }

But only _.merge and _.defaultsDeep will merge child objects

_.assign      ({}, {a:{a:'a'}}, {a:{b:'bb'}}) // => { "a": { "b": "bb" }}
_.merge       ({}, {a:{a:'a'}}, {a:{b:'bb'}}) // => { "a": { "a": "a", "b": "bb" }}
_.defaults    ({}, {a:{a:'a'}}, {a:{b:'bb'}}) // => { "a": { "a": "a" }}
_.defaultsDeep({}, {a:{a:'a'}}, {a:{b:'bb'}}) // => { "a": { "a": "a", "b": "bb" }}

And none of them will merge arrays it seems

_.assign      ({}, {a:['a']}, {a:['bb']}) // => { "a": [ "bb" ] }
_.merge       ({}, {a:['a']}, {a:['bb']}) // => { "a": [ "bb" ] }
_.defaults    ({}, {a:['a']}, {a:['bb']}) // => { "a": [ "a"  ] }
_.defaultsDeep({}, {a:['a']}, {a:['bb']}) // => { "a": [ "a"  ] }

All modify the target object

a={a:'a'}; _.assign      (a, {b:'bb'}); // a => { a: "a", b: "bb" }
a={a:'a'}; _.merge       (a, {b:'bb'}); // a => { a: "a", b: "bb" }
a={a:'a'}; _.defaults    (a, {b:'bb'}); // a => { a: "a", b: "bb" }
a={a:'a'}; _.defaultsDeep(a, {b:'bb'}); // a => { a: "a", b: "bb" }

Lodash treats arrays as objects where the keys are the index into the array.

_.assign      ([], ['a'], ['bb']) // => [ "bb" ]
_.merge       ([], ['a'], ['bb']) // => [ "bb" ]
_.defaults    ([], ['a'], ['bb']) // => [ "a"  ]
_.defaultsDeep([], ['a'], ['bb']) // => [ "a"  ]

_.assign      ([], ['a','b'], ['bb']) // => [ "bb", "b" ]
_.merge       ([], ['a','b'], ['bb']) // => [ "bb", "b" ]
_.defaults    ([], ['a','b'], ['bb']) // => [ "a", "b"  ]
_.defaultsDeep([], ['a','b'], ['bb']) // => [ "a", "b"  ]

延时执行

console.log('Start');
setTimeout(() => console.log('TO1'), 0);
setImmediate(() => console.log('IM1'));
process.nextTick(() => console.log('NT1'));
setImmediate(() => console.log('IM2'));
process.nextTick(() => console.log('NT2'));
http.get(options, () => console.log('IO1'));
fs.readdir(process.cwd(), () => console.log('IO2'));
setImmediate(() => console.log('IM3'));
process.nextTick(() => console.log('NT3'));
setImmediate(() => console.log('IM4'));
fs.readdir(process.cwd(), () => console.log('IO3'));
console.log('Done');

将会输出:

Start
Done
NT1
NT2
NT3
TO1
IO2
IO3
IM1
IM2
IM3
IM4
IO1

yarn 和 npm

在npm5之前,新项推荐使用yarn代替npm,最大的好处是对小版本的锁定,性能也好一些。

  • yarn upgrade package 可以单独升级一个包,即使这个包使用github仓库也可以(否则yarn.lock会记录commit hash,导致不升级)
  • yarn import可以按照现有node_modules目录内容生成一个yarn.lock文件,方便从npm迁移到yarn
  • 如果部分包限制node版本导致安装失败,可以用yarn –ignore-engines install,或者干脆配置一下yarn config set ignore-engines true

npm5版本引入了package-lock.json,使得yarn和npm的区别已经不大了。

arrow function

和普通function的一个巨大的差异是this,arrow function保留了this为定义时刻的this,所以在vue的组件method定义中基本上不能使用arrow function

ESLint

如果代码中引用全局变量被ESLint报错,比如Media,可以在相应文件开头上加下面的注释来临时禁止报错。

/* global Media */

VSCode

VSCode如果按照ESLint插件,可以设置以下参数以打开vue文件支持和autoFix,这个设置时针对vscode 1.36.1,高版本不适用。

"eslint.validate": [
    "javascript",
    {
      "language": "vue", 
      "autoFix": true 
    },
    "javascriptreact"
],
"eslint.autoFixOnSave": true,
"eslint.packageManager": "yarn",
"vetur.validation.template": false

上面最后一句是让vscode忽略一些<template>上的错误。

如果不想在search的时候包括一些文件,可以在设置中加入exclude,比如:

"search.exclude": {
    "**/node_modules": true,
    "**/bower_components": true,
    "**/build":true,
    "**/dist":true
  }

ES6 import/export

在ES6之前,JS的模块化有两种方式 AMD和CommonJS,ES6终于有了语言级别的模块化支持:

命名方式:

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
或者
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

default方式:

//------ myFunc.js ------
export default function () { ... };

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

混合方式:

//------ underscore.js ------
export default function (obj) {
    ...
};
export function each(obj, iterator, context) {
    ...
}
export { each as forEach };

//------ main.js ------
import _, { each } from 'underscore';

和非ES6代码的对应关系

export { Tiger }        <=> module.exports.Tiger = Tiger
module.exports = Tiger  <=> export default Tiger

现代一些js库,比如vue,会在dist里直接生成一个.esm.js文件,表示纯ES6模块文件

webpack

  • import @ 这是webpack的一个alias,用来代替src目录
  • webpack可以load json,所以可以这样import { version } from '../package.json'来导入项目package.json里的版本号。
  • webpack和rollup的对比,可以看这个小代码 https://gist.github.com/Rich-Harris/79a02519fb00837e8d5a4b24355881c0

vue

vue的目录结构建议(使用vue-router和vuex):

app/
    modelA/
        components/
            xxx.vue
        vuex/
            xxx.js
            index.js
            mutations.js
        index.js
        routes.js
    App.vue
    index.js
    routes.js
    vuex.js
assets/
router/index.js
vuex/index.js
components/     (共享的模块)
main.js
index.html

export大多通过index.js方式,写法是这样:

export { default as AAA } from './AAA';
export { default as BBB } from './BBB';

这样的好处是在import时可以比较简洁,参考这里

localStorage

使用chrome的developer tools可以查看和修改localstorage,非常方便。查看是在application tab页下,双击value可以修改。

localStorage有大小限制,一般认为2.5M是比较安全的一个限制。

chart库

  • chart.js 115k 早期版本非常轻量(10k),如果不需要新的功能,可以考虑用旧版本的chart.js
  • frappe charts 看起来不错,轻量 但是webpack打包的时候会报错Invalid assignment
  • highcharts 商用使用需要授权,75k 生成的chart右下角有个到highcharts的链接,可以通过选项去除。
  • c3.js 本身就比较大,而且还需要依赖d3

兼容性

  • 字符串的startsWith方法比较新,很多浏览器不支持。
  • promise的finally部分很多浏览器不支持
  • 使用queryNodeAll得到的NodeList不是Array,它的forEach方法也是后来加上的,所以很多浏览器不支持。