3 语言特性

3.1 变量

【强制】 变量在使用前必须通过 var 定义。

解释:

不通过 var 定义变量将导致变量污染全局环境。

示例:

  1. // good
  2. var name = 'MyName';
  3. // bad
  4. name = 'MyName';
【强制】 每个 var 只能声明一个变量。

解释:

一个 var 声明多个变量,容易导致较长的行长度,并且在修改时容易造成逗号和分号的混淆。

示例:

  1. // good
  2. var hangModules = [];
  3. var missModules = [];
  4. var visited = {};
  5. // bad
  6. var hangModules = [],
  7. missModules = [],
  8. visited = {};
【强制】 变量必须 即用即声明,不得在函数或其它形式的代码块起始位置统一声明所有变量。

解释:

变量声明与使用的距离越远,出现的跨度越大,代码的阅读与维护成本越高。虽然JavaScript的变量是函数作用域,还是应该根据编程中的意图,缩小变量出现的距离空间。

示例:

  1. // good
  2. function kv2List(source) {
  3. var list = [];
  4. for (var key in source) {
  5. if (source.hasOwnProperty(key)) {
  6. var item = {
  7. k: key,
  8. v: source[key]
  9. };
  10. list.push(item);
  11. }
  12. }
  13. return list;
  14. }
  15. // bad
  16. function kv2List(source) {
  17. var list = [];
  18. var key;
  19. var item;
  20. for (key in source) {
  21. if (source.hasOwnProperty(key)) {
  22. item = {
  23. k: key,
  24. v: source[key]
  25. };
  26. list.push(item);
  27. }
  28. }
  29. return list;
  30. }

3.2 条件

【强制】 在 Equality Expression 中使用类型严格的 ===。仅当判断 null 或 undefined 时,允许使用 == null

解释:

使用 === 可以避免等于判断中隐式的类型转换。

示例:

  1. // good
  2. if (age === 30) {
  3. // ......
  4. }
  5. // bad
  6. if (age == 30) {
  7. // ......
  8. }
【建议】 尽可能使用简洁的表达式。

示例:

  1. // 字符串为空
  2. // good
  3. if (!name) {
  4. // ......
  5. }
  6. // bad
  7. if (name === '') {
  8. // ......
  9. }
  1. // 字符串非空
  2. // good
  3. if (name) {
  4. // ......
  5. }
  6. // bad
  7. if (name !== '') {
  8. // ......
  9. }
  1. // 数组非空
  2. // good
  3. if (collection.length) {
  4. // ......
  5. }
  6. // bad
  7. if (collection.length > 0) {
  8. // ......
  9. }
  1. // 布尔不成立
  2. // good
  3. if (!notTrue) {
  4. // ......
  5. }
  6. // bad
  7. if (notTrue === false) {
  8. // ......
  9. }
  1. // null 或 undefined
  2. // good
  3. if (noValue == null) {
  4. // ......
  5. }
  6. // bad
  7. if (noValue === null || typeof noValue === 'undefined') {
  8. // ......
  9. }
【建议】 按执行频率排列分支的顺序。

解释:

按执行频率排列分支的顺序好处是:

  1. 阅读的人容易找到最常见的情况,增加可读性。
  2. 提高执行效率。
【建议】 对于相同变量或表达式的多值条件,用 switch 代替 if

示例:

  1. // good
  2. switch (typeof variable) {
  3. case 'object':
  4. // ......
  5. break;
  6. case 'number':
  7. case 'boolean':
  8. case 'string':
  9. // ......
  10. break;
  11. }
  12. // bad
  13. var type = typeof variable;
  14. if (type === 'object') {
  15. // ......
  16. }
  17. else if (type === 'number' || type === 'boolean' || type === 'string') {
  18. // ......
  19. }
【建议】 如果函数或全局中的 else 块后没有任何语句,可以删除 else

示例:

  1. // good
  2. function getName() {
  3. if (name) {
  4. return name;
  5. }
  6. return 'unnamed';
  7. }
  8. // bad
  9. function getName() {
  10. if (name) {
  11. return name;
  12. }
  13. else {
  14. return 'unnamed';
  15. }
  16. }

3.3 循环

【建议】 不要在循环体中包含函数表达式,事先将函数提取到循环体外。

解释:

循环体中的函数表达式,运行过程中会生成循环次数个函数对象。

示例:

  1. // good
  2. function clicker() {
  3. // ......
  4. }
  5. for (var i = 0, len = elements.length; i < len; i++) {
  6. var element = elements[i];
  7. addListener(element, 'click', clicker);
  8. }
  9. // bad
  10. for (var i = 0, len = elements.length; i < len; i++) {
  11. var element = elements[i];
  12. addListener(element, 'click', function () {});
  13. }
【建议】 对循环内多次使用的不变值,在循环外用变量缓存。

示例:

  1. // good
  2. var width = wrap.offsetWidth + 'px';
  3. for (var i = 0, len = elements.length; i < len; i++) {
  4. var element = elements[i];
  5. element.style.width = width;
  6. // ......
  7. }
  8. // bad
  9. for (var i = 0, len = elements.length; i < len; i++) {
  10. var element = elements[i];
  11. element.style.width = wrap.offsetWidth + 'px';
  12. // ......
  13. }
【建议】 对有序集合进行遍历时,缓存 length

解释:

虽然现代浏览器都对数组长度进行了缓存,但对于一些宿主对象和老旧浏览器的数组对象,在每次 length 访问时会动态计算元素个数,此时缓存 length 能有效提高程序性能。

示例:

  1. for (var i = 0, len = elements.length; i < len; i++) {
  2. var element = elements[i];
  3. // ......
  4. }
【建议】 对有序集合进行顺序无关的遍历时,使用逆序遍历。

解释:

逆序遍历可以节省变量,代码比较优化。

示例:

  1. var len = elements.length;
  2. while (len--) {
  3. var element = elements[len];
  4. // ......
  5. }

3.4 类型

3.4.1 类型检测

【建议】 类型检测优先使用 typeof。对象类型检测使用 instanceofnullundefined 的检测使用 == null

示例:

  1. // string
  2. typeof variable === 'string'
  3. // number
  4. typeof variable === 'number'
  5. // boolean
  6. typeof variable === 'boolean'
  7. // Function
  8. typeof variable === 'function'
  9. // Object
  10. typeof variable === 'object'
  11. // RegExp
  12. variable instanceof RegExp
  13. // Array
  14. variable instanceof Array
  15. // null
  16. variable === null
  17. // null or undefined
  18. variable == null
  19. // undefined
  20. typeof variable === 'undefined'

3.4.2 类型转换

【建议】 转换成 string 时,使用 + ''

示例:

  1. // good
  2. num + '';
  3. // bad
  4. new String(num);
  5. num.toString();
  6. String(num);
【建议】 转换成 number 时,通常使用 +

示例:

  1. // good
  2. +str;
  3. // bad
  4. Number(str);
【建议】 string 转换成 number,要转换的字符串结尾包含非数字并期望忽略时,使用 parseInt

示例:

  1. var width = '200px';
  2. parseInt(width, 10);
【强制】 使用 parseInt 时,必须指定进制。

示例:

  1. // good
  2. parseInt(str, 10);
  3. // bad
  4. parseInt(str);
【建议】 转换成 boolean 时,使用 !!

示例:

  1. var num = 3.14;
  2. !!num;
【建议】 number 去除小数点,使用 Math.floor / Math.round / Math.ceil,不使用 parseInt

示例:

  1. // good
  2. var num = 3.14;
  3. Math.ceil(num);
  4. // bad
  5. var num = 3.14;
  6. parseInt(num, 10);

3.5 字符串

【强制】 字符串开头和结束使用单引号 '

解释:

  1. 输入单引号不需要按住 shift,方便输入。
  2. 实际使用中,字符串经常用来拼接 HTML。为方便 HTML 中包含双引号而不需要转义写法。

示例:

  1. var str = '我是一个字符串';
  2. var html = '<div class="cls">拼接HTML可以省去双引号转义</div>';
【建议】 使用 数组+ 拼接字符串。

解释:

  1. 使用 + 拼接字符串,如果拼接的全部是 StringLiteral,压缩工具可以对其进行自动合并的优化。所以,静态字符串建议使用 + 拼接。
  2. 在现代浏览器下,使用 + 拼接字符串,性能较数组的方式要高。
  3. 如需要兼顾老旧浏览器,应尽量使用数组拼接字符串。

示例:

  1. // 使用数组拼接字符串
  2. var str = [
  3. // 推荐换行开始并缩进开始第一个字符串, 对齐代码, 方便阅读.
  4. '<ul>',
  5. '<li>第一项</li>',
  6. '<li>第二项</li>',
  7. '</ul>'
  8. ].join('');
  9. // 使用 + 拼接字符串
  10. var str2 = '' // 建议第一个为空字符串, 第二个换行开始并缩进开始, 对齐代码, 方便阅读
  11. + '<ul>',
  12. + '<li>第一项</li>',
  13. + '<li>第二项</li>',
  14. + '</ul>';
【建议】 复杂的数据到视图字符串的转换过程,选用一种模板引擎。

解释:

使用模板引擎有如下好处:

  1. 在开发过程中专注于数据,将视图生成的过程由另外一个层级维护,使程序逻辑结构更清晰。
  2. 优秀的模板引擎,通过模板编译技术和高质量的编译产物,能获得比手工拼接字符串更高的性能。
  • artTemplate: 体积较小,在所有环境下性能高,语法灵活。
  • dot.js: 体积小,在现代浏览器下性能高,语法灵活。
  • etpl: 体积较小,在所有环境下性能高,模板复用性高,语法灵活。
  • handlebars: 体积大,在所有环境下性能高,扩展性高。
  • hogon: 体积小,在现代浏览器下性能高。
  • nunjucks: 体积较大,性能一般,模板复用性高。

3.6 对象

【强制】 使用对象字面量 {} 创建新 Object

示例:

  1. // good
  2. var obj = {};
  3. // bad
  4. var obj = new Object();
【强制】 对象创建时,如果一个对象的所有 属性 均可以不添加引号,则所有 属性 不得添加引号。

示例:

  1. var info = {
  2. name: 'someone',
  3. age: 28
  4. };
【强制】 对象创建时,如果任何一个 属性 需要添加引号,则所有 属性 必须添加 '

解释:

如果属性不符合 Identifier 和 NumberLiteral 的形式,就需要以 StringLiteral 的形式提供。

示例:

  1. // good
  2. var info = {
  3. 'name': 'someone',
  4. 'age': 28,
  5. 'more-info': '...'
  6. };
  7. // bad
  8. var info = {
  9. name: 'someone',
  10. age: 28,
  11. 'more-info': '...'
  12. };
【强制】 不允许修改和扩展任何原生对象和宿主对象的原型。

示例:

  1. // 以下行为绝对禁止
  2. String.prototype.trim = function () {
  3. };
【建议】 属性访问时,尽量使用 .

解释:

属性名符合 Identifier 的要求,就可以通过 . 来访问,否则就只能通过 [expr] 方式访问。

通常在 JavaScript 中声明的对象,属性命名是使用 Camel 命名法,用 . 来访问更清晰简洁。部分特殊的属性(比如来自后端的JSON),可能采用不寻常的命名方式,可以通过 [expr] 方式访问。

示例:

  1. info.age;
  2. info['more-info'];
【建议】 for in 遍历对象时, 使用 hasOwnProperty 过滤掉原型中的属性。

示例:

  1. var newInfo = {};
  2. for (var key in info) {
  3. if (info.hasOwnProperty(key)) {
  4. newInfo[key] = info[key];
  5. }
  6. }

3.7 数组

【强制】 使用数组字面量 [] 创建新数组,除非想要创建的是指定长度的数组。

示例:

  1. // good
  2. var arr = [];
  3. // bad
  4. var arr = new Array();
【强制】 遍历数组不使用 for in

解释:

数组对象可能存在数字以外的属性, 这种情况下 for in 不会得到正确结果.

示例:

  1. var arr = ['a', 'b', 'c'];
  2. arr.other = 'other things'; // 这里仅作演示, 实际中应使用Object类型
  3. // 正确的遍历方式
  4. for (var i = 0, len = arr.length; i < len; i++) {
  5. console.log(i);
  6. }
  7. // 错误的遍历方式
  8. for (i in arr) {
  9. console.log(i);
  10. }
【建议】 不因为性能的原因自己实现数组排序功能,尽量使用数组的 sort 方法。

解释:

自己实现的常规排序算法,在性能上并不优于数组默认的 sort 方法。以下两种场景可以自己实现排序:

  1. 需要稳定的排序算法,达到严格一致的排序结果。
  2. 数据特点鲜明,适合使用桶排。
【建议】 清空数组使用 .length = 0

3.8 函数

3.8.1 函数长度

【建议】 一个函数的长度控制在 50 行以内。

解释:

将过多的逻辑单元混在一个大函数中,易导致难以维护。一个清晰易懂的函数应该完成单一的逻辑单元。复杂的操作应进一步抽取,通过函数的调用来体现流程。

特定算法等不可分割的逻辑允许例外。

示例:

  1. function syncViewStateOnUserAction() {
  2. if (x.checked) {
  3. y.checked = true;
  4. z.value = '';
  5. }
  6. else {
  7. y.checked = false;
  8. }
  9. if (!a.value) {
  10. warning.innerText = 'Please enter it';
  11. submitButton.disabled = true;
  12. }
  13. else {
  14. warning.innerText = '';
  15. submitButton.disabled = false;
  16. }
  17. }
  18. // 直接阅读该函数会难以明确其主线逻辑,因此下方是一种更合理的表达方式:
  19. function syncViewStateOnUserAction() {
  20. syncXStateToView();
  21. checkAAvailability();
  22. }
  23. function syncXStateToView() {
  24. if (x.checked) {
  25. y.checked = true;
  26. z.value = '';
  27. }
  28. else {
  29. y.checked = false;
  30. }
  31. }
  32. function checkAAvailability() {
  33. if (!a.value) {
  34. displayWarningForAMissing();
  35. }
  36. else {
  37. clearWarnignForA();
  38. }
  39. }

3.8.2 参数设计

【建议】 一个函数的参数控制在 6 个以内。

解释:

除去不定长参数以外,函数具备不同逻辑意义的参数建议控制在 6 个以内,过多参数会导致维护难度增大。

【建议】 通过 options 参数传递非数据输入型参数。

解释:

有些函数的参数并不是作为算法的输入,而是对算法的某些分支条件判断之用,此类参数建议通过一个 options 参数传递。

如下函数:

  1. /**
  2. * 移除某个元素
  3. *
  4. * @param {Node} element 需要移除的元素
  5. * @param {boolean} removeEventListeners 是否同时将所有注册在元素上的事件移除
  6. */
  7. function removeElement(element, removeEventListeners) {
  8. element.parent.removeChild(element);
  9. if (removeEventListeners) {
  10. element.clearEventListeners();
  11. }
  12. }

可以转换为下面的签名:

  1. /**
  2. * 移除某个元素
  3. *
  4. * @param {Node} element 需要移除的元素
  5. * @param {Object} options 相关的逻辑配置
  6. * @param {boolean} options.removeEventListeners 是否同时将所有注册在元素上的事件移除
  7. */
  8. function removeElement(element, options) {
  9. element.parent.removeChild(element);
  10. if (options.removeEventListeners) {
  11. element.clearEventListeners();
  12. }
  13. }

这种模式有几个显著的优势:

  • boolean 型的配置项具备名称,从调用的代码上更易理解其表达的逻辑意义。
  • 当配置项有增长时,无需无休止地增加参数个数,不会出现 removeElement(element, true, false, false, 3) 这样难以理解的调用代码。
  • 当部分配置参数可选时,多个参数的形式非常难处理重载逻辑,而使用一个 options 对象只需判断属性是否存在,实现得以简化。

3.8.3 闭包

【建议】 在适当的时候将闭包内大对象置为 null

解释:

在 JavaScript 中,无需特别的关键词就可以使用闭包,一个函数可以任意访问在其定义的作用域外的变量。需要注意的是,函数的作用域是静态的,即在定义时决定,与调用的时机和方式没有任何关系。

闭包会阻止一些变量的垃圾回收,对于较老旧的JavaScript引擎,可能导致外部所有变量均无法回收。

首先一个较为明确的结论是,以下内容会影响到闭包内变量的回收:

  • 嵌套的函数中是否有使用该变量。
  • 嵌套的函数中是否有 直接调用eval
  • 是否使用了 with 表达式。

Chakra、V8 和 SpiderMonkey 将受以上因素的影响,表现出不尽相同又较为相似的回收策略,而JScript.dll和Carakan则完全没有这方面的优化,会完整保留整个 LexicalEnvironment 中的所有变量绑定,造成一定的内存消耗。

由于对闭包内变量有回收优化策略的 Chakra、V8 和 SpiderMonkey 引擎的行为较为相似,因此可以总结如下,当返回一个函数 fn 时:

  1. 如果 fn 的 [[Scope]] 是ObjectEnvironment(with 表达式生成 ObjectEnvironment,函数和 catch 表达式生成 DeclarativeEnvironment),则:
    1. 如果是 V8 引擎,则退出全过程。
    2. 如果是 SpiderMonkey,则处理该 ObjectEnvironment 的外层 LexicalEnvironment。
  2. 获取当前 LexicalEnvironment 下的所有类型为 Function 的对象,对于每一个 Function 对象,分析其 FunctionBody:
    1. 如果 FunctionBody 中含有 直接调用eval,则退出全过程。
    2. 否则得到所有的 Identifier。
    3. 对于每一个 Identifier,设其为 name,根据查找变量引用的规则,从 LexicalEnvironment 中找出名称为 name 的绑定 binding。
    4. 对 binding 添加 notSwap 属性,其值为 true。
  3. 检查当前 LexicalEnvironment 中的每一个变量绑定,如果该绑定有 notSwap 属性且值为 true,则:
    1. 如果是V8引擎,删除该绑定。
    2. 如果是SpiderMonkey,将该绑定的值设为 undefined,将删除 notSwap 属性。

对于Chakra引擎,暂无法得知是按 V8 的模式还是按 SpiderMonkey 的模式进行。

如果有 非常庞大 的对象,且预计会在 老旧的引擎 中执行,则使用闭包时,注意将闭包不需要的对象置为空引用。

【建议】 使用 IIFE 避免 Lift 效应

解释:

在引用函数外部变量时,函数执行时外部变量的值由运行时决定而非定义时,最典型的场景如下:

  1. var tasks = [];
  2. for (var i = 0; i < 5; i++) {
  3. tasks[tasks.length] = function () {
  4. console.log('Current cursor is at ' + i);
  5. };
  6. }
  7. var len = tasks.length;
  8. while (len--) {
  9. tasks[len]();
  10. }

以上代码对 tasks 中的函数的执行均会输出 Current cursor is at 5,往往不符合预期。

此现象称为 Lift 效应 。解决的方式是通过额外加上一层闭包函数,将需要的外部变量作为参数传递来解除变量的绑定关系:

  1. var tasks = [];
  2. for (var i = 0; i < 5; i++) {
  3. // 注意有一层额外的闭包
  4. tasks[tasks.length] = (function (i) {
  5. return function () {
  6. console.log('Current cursor is at ' + i);
  7. };
  8. })(i);
  9. }
  10. var len = tasks.length;
  11. while (len--) {
  12. tasks[len]();
  13. }

3.8.4 空函数

【建议】 空函数不使用 new Function() 的形式。

示例:

  1. var emptyFunction = function () {};
【建议】 对于性能有高要求的场合,建议存在一个空函数的常量,供多处使用共享。

示例:

  1. var EMPTY_FUNCTION = function () {};
  2. function MyClass() {
  3. }
  4. MyClass.prototype.abstractMethod = EMPTY_FUNCTION;
  5. MyClass.prototype.hooks.before = EMPTY_FUNCTION;
  6. MyClass.prototype.hooks.after = EMPTY_FUNCTION;

3.9 面向对象

【强制】 类的继承方案,实现时需要修正 constructor

解释:

通常使用其他 library 的类继承方案都会进行 constructor 修正。如果是自己实现的类继承方案,需要进行 constructor 修正。

示例:

  1. /**
  2. * 构建类之间的继承关系
  3. *
  4. * @param {Function} subClass 子类函数
  5. * @param {Function} superClass 父类函数
  6. */
  7. function inherits(subClass, superClass) {
  8. var F = new Function();
  9. F.prototype = superClass.prototype;
  10. subClass.prototype = new F();
  11. subClass.prototype.constructor = subClass;
  12. }
【建议】 声明类时,保证 constructor 的正确性。

示例:

  1. function Animal(name) {
  2. this.name = name;
  3. }
  4. // 直接prototype等于对象时,需要修正constructor
  5. Animal.prototype = {
  6. constructor: Animal,
  7. jump: function () {
  8. alert('animal ' + this.name + ' jump');
  9. }
  10. };
  11. // 这种方式扩展prototype则无需理会constructor
  12. Animal.prototype.jump = function () {
  13. alert('animal ' + this.name + ' jump');
  14. };
【建议】 属性在构造函数中声明,方法在原型中声明。

解释:

原型对象的成员被所有实例共享,能节约内存占用。所以编码时我们应该遵守这样的原则:原型对象包含程序不会修改的成员,如方法函数或配置项。

  1. function TextNode(value, engine) {
  2. this.value = value;
  3. this.engine = engine;
  4. }
  5. TextNode.prototype.clone = function () {
  6. return this;
  7. };
【强制】 自定义事件的 事件名 必须全小写。

解释:

在 JavaScript 广泛应用的浏览器环境,绝大多数 DOM 事件名称都是全小写的。为了遵循大多数 JavaScript 开发者的习惯,在设计自定义事件时,事件名也应该全小写。

【强制】 自定义事件只能有一个 event 参数。如果事件需要传递较多信息,应仔细设计事件对象。

解释:

一个事件对象的好处有:

  1. 顺序无关,避免事件监听者需要记忆参数顺序。
  2. 每个事件信息都可以根据需要提供或者不提供,更自由。
  3. 扩展方便,未来添加事件信息时,无需考虑会破坏监听器参数形式而无法向后兼容。
【建议】 设计自定义事件时,应考虑禁止默认行为。

解释:

常见禁止默认行为的方式有两种:

  1. 事件监听函数中 return false。
  2. 事件对象中包含禁止默认行为的方法,如 preventDefault。

3.10 动态特性

3.10.1 eval

【强制】 避免使用直接 eval 函数。

解释:

直接 eval,指的是以函数方式调用 eval 的调用方法。直接 eval 调用执行代码的作用域为本地作用域,应当避免。

如果有特殊情况需要使用直接 eval,需在代码中用详细的注释说明为何必须使用直接 eval,不能使用其它动态执行代码的方式,同时需要其他资深工程师进行 Code Review。

【建议】 尽量避免使用 eval 函数。

3.10.2 动态执行代码

【建议】 使用 new Function 执行动态代码。

解释:

通过 new Function 生成的函数作用域是全局使用域,不会影响当当前的本地作用域。如果有动态代码执行的需求,建议使用 new Function。

示例:

  1. var handler = new Function('x', 'y', 'return x + y;');
  2. var result = handler($('# x').val(), $('# y').val());

3.10.3 with

【建议】 尽量不要使用 with

解释:

使用 with 可能会增加代码的复杂度,不利于阅读和管理;也会对性能有影响。大多数使用 with 的场景都能使用其他方式较好的替代。所以,尽量不要使用 with。

3.10.4 delete

【建议】 减少 delete 的使用。

解释:

如果没有特别的需求,减少或避免使用deletedelete的使用会破坏部分 JavaScript 引擎的性能优化。

【建议】 处理 delete 可能产生的异常。

解释:

对于有被遍历需求,且值 null 被认为具有业务逻辑意义的值的对象,移除某个属性必须使用 delete 操作。

在严格模式或IE下使用 delete 时,不能被删除的属性会抛出异常,因此在不确定属性是否可以删除的情况下,建议添加 try-catch 块。

示例:

  1. try {
  2. delete o.x;
  3. }
  4. catch (deleteError) {
  5. o.x = null;
  6. }

3.10.5 对象属性

【建议】 避免修改外部传入的对象。

解释:

JavaScript 因其脚本语言的动态特性,当一个对象未被 seal 或 freeze 时,可以任意添加、删除、修改属性值。

但是随意地对 非自身控制的对象 进行修改,很容易造成代码在不可预知的情况下出现问题。因此,设计良好的组件、函数应该避免对外部传入的对象的修改。

下面代码的 selectNode 方法修改了由外部传入的 datasource 对象。如果 datasource 用在其它场合(如另一个 Tree 实例)下,会造成状态的混乱。

  1. function Tree(datasource) {
  2. this.datasource = datasource;
  3. }
  4. Tree.prototype.selectNode = function (id) {
  5. // 从datasource中找出节点对象
  6. var node = this.findNode(id);
  7. if (node) {
  8. node.selected = true;
  9. this.flushView();
  10. }
  11. };

对于此类场景,需要使用额外的对象来维护,使用由自身控制,不与外部产生任何交互的 selectedNodeIndex 对象来维护节点的选中状态,不对 datasource 作任何修改。

  1. function Tree(datasource) {
  2. this.datasource = datasource;
  3. this.selectedNodeIndex = {};
  4. }
  5. Tree.prototype.selectNode = function (id) {
  6. // 从datasource中找出节点对象
  7. var node = this.findNode(id);
  8. if (node) {
  9. this.selectedNodeIndex[id] = true;
  10. this.flushView();
  11. }
  12. };

除此之外,也可以通过 deepClone 等手段将自身维护的对象与外部传入的分离,保证不会相互影响。

【建议】 具备强类型的设计。

解释:

  • 如果一个属性被设计为 boolean 类型,则不要使用 1 / 0 作为其值。对于标识性的属性,如对代码体积有严格要求,可以从一开始就设计为 number 类型且将 0 作为否定值。
  • 从 DOM 中取出的值通常为 string 类型,如果有对象或函数的接收类型为 number 类型,提前作好转换,而不是期望对象、函数可以处理多类型的值。