<<编写可维护的javascript>> part2: 编程实践
chapter5 UI层的松耦合
- css 去 javascript
> 不要用css 的expression 表达式
- javascript 去 css
1 2 3 4 5 6 | element.style.color = "red" ; // 不好的写法 element.style.cssText= "color: red; left: 10px;" ; // 不好的写法 element.className += " msg-color" ; // 好的写法 element.classList.add( "msg-color" ); // 好的写法 html5 $(id).addClass( "msg-color" ); // 好的写法 |
- html 去 javascript
1 2 3 4 | <a onclick= "doSth()" /> // 不好的写法, DOM0 模型 <script> ... // 不好的写法, 但是这样写可以减少http请求 </script> |
- javascript 去 html
> 服务器加载: 从服务器端获取, 容易造成XSS 漏洞
> 复杂模版: 用模版引擎如 Handlebars.js, [注意]获取模版时用jquery.html()方法而不是text()方法, 否则ie8会有问题
chapter6 避免全局变量
- 全局变量的问题
> 命名冲突
> 难以测试
- 单全局变量模式: 参考<<javscipt 设计模式>>
> 命名空间
> 命名空间
>> 在一个全局变量的基础上, 增加命名空间
1 2 3 | var haili = {}; haili.mySpace1 = {}; // 命名空间1, 以后属于命名空间1的模块都写在这里 haili.mySpace2 = {}; // 命名空间2, 以后属于命名空间2的模块都写在这里 |
> 模块
- 零全局变量, 在一个匿名函数内执行, 不常用
chapter7 事件处理
- 典型用法, 不太好
1 2 3 4 5 | function handleClick(event) { // 不好的用法 var popup = document,getElementById( "popup" ); // 应用逻辑 popup.style.left = event.clientX + "px" ; // event.clentX 为事件处理 } addListener(element, "click" , handleClick); |
- 好的用法
> [优点]
>> 隔离了应用逻辑, 提高了可重用性
>> 阻止了分发event对象, 提高了便于测试, 方法可重用性, 代码可读性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | var myApp = { handleClick: funtion(event) { // 事件处理 // 阻止冒泡, 组织默认行为 event.preventDefault(); event.stopPropagation(); // 传入应用逻辑 this .showPopup(event.clientX); }, showPopup: functon(x) { // 应用逻辑 var popup = document,getElementById( "popup" ); popup.style.left = x + "px" ; } }; addListener(element, "click" , function (event) { myApp.handleClick(event); }); |
chapter8 避免空比较
- 监测原始值
> 用 typeof 来检查原始类型, 来确保安全, [字符串, 数字, 布尔值, undefined]
> 不要用typeof 来检测null! 不要用typeof 来检测null! 不要用typeof 来检测null! 用 === | !==
1 | typeof null ; // "Object" |
- 监测引用值
> 用 instanceof来检查引用类型
> instanceof 不能跨帧(frame)
1 2 3 4 5 6 7 8 9 10 11 12 | // frame1 和frame2 是浏览器的两个帧, 拥有相同的构造函数Persion // frame1: function Persion() {} var man1 = new Persion();
// frame2: function Persion() {} var man2 = new Persion(); // 则以下成立 man1 instanceof frame1Persion // true , man1 属于 frame1的 Persion man1 instanceof frame2Persion // false , man1 不属于 frame2的 Persion |
- 检测函数
> typeof可以跨帧(frame)
1 2 3 4 5 | // frame1 function func() {} // frame 2 typeof func = "function" |
> 然而ie8 - 浏览器中, typeof 检测dom节点的函数时返回的是object
1 2 | // ie8- typeof document.getElementById = "object" |
- 监测数组
> 用 Array.isArray() 方法
- 检测属性在不在对象中
> 用 in, in大法好
> 用hasOwnProperty(),但是在ie8-中,DOM对象并非继承Object. 所以不包含这个方法
chapter9 配置数据从代码分离
- 抽离配置数据: [URL, 重复的值, 设置...]
> 用对象保存
1 2 3 4 5 6 7 8 | var config = { URL_INVALID: "baidu.com" , CSS_SELECTED: "sleected" }; function select() { var css = config.CSS_SELECTED; // 应用配置数据 } |
chapter10 抛出自定义异常
- 抛异常的意义
> 正确的抛异常可以帮助我们找到错误的地方
> 对旧的ie 找到错误有较好的帮助
> 只在必要的地方抛
> 错误类型: [Error, EvalError, TypeError, RangeError, ...]
- thorw
1 | throw new Error( "this is error message" ); // 抛出错误 |
- try-catch
1 2 3 4 5 | try { .... } catch (e) { // 不要留空, 要写点东西 } |
chapter11 不是你的对象不要动
- 如果你的代码没有创建这些对象, 不要修改他们, 不要修改他们, 不要修改他们, 包括:
> 原生对象(Object, Array...)
> DOM 对象 (document)
> BOM 对象 (window)
> 类库对象
- 原则
> 不覆盖方法
1 | document.getElementById = function () {}; // 不好的写法, 什么鬼, 其他人怎么知道这个改了, 错都不知道怎么找 |
> 不新增方法, [缺点]如下
>> 命名冲突
>> 如果将来原生的方法和你的方法行为不一致, 后果很悲催 (血淋淋的例子-> prototype.js 的getElementByClassName 方法)
1 2 3 4 5 6 7 8 9 | document.sayHi = function () { // 不好的写法, 在DOM 上新增了方法 alert( "hi" ); }; Array.prototype.sayHi = function () { // 不好的写法, 在原生对象上新增了方法 alert( "hi" ); }; EasyUI.sayHi = function () { // 不好的写法, 在EasyUI库对象上新增了方法 alert( "hi" ); }; |
> 不删除方法
1 2 | document.getElemetnById = null ; // 不好的写法, 删掉了DOM方法 detete document.getElementById; // 不影响, 仍能工作, 因为getElementById是原型链上的一个方法 |
- 更好的解决办法, 用最受欢迎的继承来扩充对象
> 基于对象的继承(原型继承)
>> 原理是创建一个属于你的对象, 就可以随意修改了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var Person = { // 这是原来的别人的对象 name: "" , age: "" , sayName: function () { console.log( "my name is " + this .name + ", i'm " + this .age); } }; // 对Person 进行原型继承, 创建haili这个新对象, 属于我, 可以随意修改 // 第二个参数对原来对象的属性进行覆盖 var haili = Object.create(Person, { // ECMAScript 5 才有 create() 方法 name: { value: "xiaoming" } }); haili.sayName(); // my name is xiaoming, i'm |
> 基于类型的继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function Person(name) { // 别人家的对象 this .name = name; } function MyPerson(name, age) { // 我的对象 this .age =age; Person.call( this , name, age); } MyPerson.prototype.sayName = function () { // 我给我的对象加了个方法, 不影响别人的对象 console.log( "my name is " + this .name + ", i'm " + this .age); }; var haili = new MyPerson( "haili" , 10); // 成功 haili.sayName(); |
> 门面模式 (复制一份)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 原本的 dom 元素会保留下来 function DOMWrapper(domEle) { this .domEle = domEle; // 用构造函数创建一个副本, 保留了旧的对象 } // 新增新的方法给dom元素 DOMWrapper.prototype.addClass = function (className) { this .domEle.className += " " + className; }; // 新增新的方法给dom元素 DOMWrapper.prototype.remove = function () { this .domEle.parentNode.removeChild( this .domEle); }; var wrapper = new DOMWrapper(document.getElementById( "test" )); warpper.addClass( "class1" ); // 调用自定义的方法而不会影响dom的原生方法 wrapper.remove(); // 调用自定义的方法而不会影响dom的原生方法 |
- 关于 polyfill 和 shim: http://www.ituring.com.cn/article/details/766#
> [shim概念]: 一个shim是一个库,它将一个新的API引入到一个旧的环境中,而且仅靠旧环境中已有的手段实现.(一种模仿未来API并为旧浏览器提供后备功能的“衬垫”)
> [polyfill概念]: 一个polyfill是一段代码(或者插件),提供了那些开发者们希望浏览器原生提供支持的功能.因此,一个polyfill就是一个用在浏览器API上的shim.我们通常的做法是先检查当前浏览器是否支持某个API,如果不支持的话就加载对应的polyfill.然后新旧浏览器就都可以使用这个API了.
[例如] shim: 比如 jQuery 的 $.ajax 封装了 XMLHttpRequest 和 IE 用 ActiveXObject 方式创建 xhr 对象;polyfill 特指 shim 成的 api 是遵循标准的,其典型做法是在IE浏览器中增加 window.XMLHttpRequest ,内部实现使用 ActiveXObject。在实际中为了方便做对比,会特指 shim 的 api 不是遵循标准的,而是自己设计的。
>为了提高可维护性, 还是尽量避免使用polyfill(自行脑补的血淋淋的例子-> prototype.js 的getElementByClassName 方法?)
- 阻止修改
> 防止扩展: 禁止添加属性和方法, 但是已经属性存在的方法和属性可以修改和删除
1 2 | Object.preventExtension(persion); // 防止扩展, 禁止添加属性和方法, 但是已经属性存在的方法和属性可以修改 Object.isExtensible(persion); // 检测是否是可扩展, 这里是fals |
> 密封: 子集是"防止扩展", 而且增加了对象可以删除
1 2 | Object.seal(persion); // 子集是"防止扩展", 而且增加了对象可以删除 Object.isSealed(persion); // 检测是否是密封, 这里是true |
> 冻结: 子集是"密封", 而且禁止对象"修改"已经存在的属性和方法
1 2 | Object.frezz(persion); // 子集是"密封", 而且禁止对象"修改"已经存在的属性和方法Object.isFrozon(persion); // 检测是否是冻结, 这里是true |
chapter12 浏览器嗅探
- user-agent 监测
> 浏览器的user-agent不总是正确的, 有可能呗其他工具修改
1 2 3 | if (navigator.userAgent.indexOf( "MSIE" ) > -1) { // 不好的写法 // is ie } |
- 特性检测: 特性检测大法好
> 针对浏览器特有的功能特性来监测浏览器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 不好的写法, 先判断浏览器类型 if (navigator.userAgent.indexOf( "MSIE" ) > -1) { // is ie element = document.all[id]; } // 好的写法, 只针对某特定的功能在各个浏览器下有没有, 有就执行它, 木有就执行相应的备用方法 function getById(id) { var element = null ; if (document.getElementById) { // DOM ie5+ netscape 6+ 之后都支持了, 所以这个写在最前面 element = document.getElementById(id); } else if (document.all) { // ie, 只是作为备用 element = document.all[id]; } else if (document.layer) { // netscape <=4, 只是作为备用 element = document.layer[id]; } return element; } |
- 特性推断: 不恰当的使用特性检测, 应该避免
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 不好的写法 function getById(id) { var element = null ; if (document.getElementByTagName) { // 用getElementByTagName来推断getElementById element = document.getElementById(id); } else if (window.ActiveXObject) { // ie element = document.all[id]; } else { // netscape <=4, element = document.layer[id]; } return element; } |
- 浏览器推断: 不恰当的使用特性检测和特性检测, 应该避免
1 2 3 4 | // 不好的写法, 呐, 就是先判断是哪个浏览器再去执行相应动作 if (navigator.userAgent.indexOf( "MSIE" ) > -1) { // is ie element = document.all[id]; } |