├── JS类和继承.md ├── JS设计模式.md ├── JavaScript权威指南.docx ├── JavaScript高级程序设计.docx ├── images ├── Flyweight.png ├── MVC Model.png ├── MVC Model2.png ├── MVC.png ├── MVP.png ├── MVVM.png ├── Observer.png ├── PublishSubscribe.png └── pubsub.png ├── js.html ├── visio ├── Flyweight.vsdx ├── MVC.vsdx ├── MVP.vsdx ├── MVVM.vsdx ├── Observer.vsdx ├── PublishSubscribe.vsdx └── pubsub.vsdx └── 高性能JS.md /JS类和继承.md: -------------------------------------------------------------------------------- 1 | # js类和继承 2 | 3 | 4 | # JS设计模式 5 | 6 | ## 类、原型和继承 7 | 8 | ### ES5中类的继承 9 | 10 | #### 类(构造函数) 11 | 12 | 构造函数的名字通常用作类名,构造函数是类的公有标识 13 | 14 | ``` javascript 15 | // Person类 16 | function Person(name) { 17 | // 实例属性 18 | this.name = name 19 | // 实例方法 20 | this.getName = function() { 21 | return this.name 22 | } 23 | } 24 | 25 | // 类创建的对象叫类的实例对象 26 | var person = new Person('ziyi2') 27 | ``` 28 | 29 | 类的实例对象都有一个不可枚举的属性constructor属性指向类(构造函数) 30 | 31 | ``` javascript 32 | // true 33 | console.log(person.constructor === Person) 34 | ``` 35 | 36 | 构造函数是类的公有标识,但是原型是类的唯一标识 37 | 38 | ``` javascript 39 | // 检测实例对象所在的类 true 40 | // 实际上instanceof运算符并不会检测person是否由Person()构造函数初始化而来 41 | // 而是会检查person是否继承自Person.prototype 42 | console.log(person instanceof Person) 43 | // 继承至原型链顶端的Object类 true 44 | console.log(person instanceof Object) 45 | ``` 46 | 47 | 所有实例的实例方法并不是同一个 48 | 49 | ``` javascript 50 | var person1 = new Person('ziyi1') 51 | // 创建一个实例对象就会创建一个新的对象物理空间 false 52 | console.log(person1.getName === person.getName) 53 | ``` 54 | 55 | 如果不用new关键字,Person是一个普通的全局作用域中的函数 56 | 57 | ``` javascript 58 | Person('ziyi2') 59 | // 全局作用域中调用函数的this指向window全局对象 ziyi2 60 | console.log(window.name) 61 | // ziyi2 62 | console.log(window.getName()) 63 | ``` 64 | 65 | 注意类的静态属性和静态方法和实例的属性和方法的区别 66 | 67 | ``` javascript 68 | // 类属性 69 | Person.age = 23 70 | // 类方法 71 | Person.getAge = function() { 72 | return this.age 73 | } 74 | ``` 75 | 76 | 构造函数创建实例对象的过程和工厂模式类似 77 | 78 | ``` javascript 79 | function createPerson(name) { 80 | var person = new Object() 81 | person.name = name 82 | person.getName = function() { 83 | return this.name 84 | } 85 | return person 86 | } 87 | 88 | let person1 = createPerson('ziyi1') 89 | let person2 = createPerson('ziyi2') 90 | // false 91 | console.log(person1.getName === person2.getName) 92 | ``` 93 | 94 | > 工厂模式虽然抽象了创建具体对象的过程,解决了创建多个相似对象的问题,但是没有解决对象的识别问题,即如何知道对象的类型,而类(构造函数)创建的实例对象可以识别实例对象对应哪个原型对象(需要注意原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象,才属于同一个类的实例,而构造函数并不能作为类的唯一标识)。 95 | 96 | 构造函数的创建过程 97 | 98 | - 创建一个新对象 99 | - 将构造函数的作用域赋给新对象(this新对象) 100 | - 执行构造函数中的代码 101 | - 返回新对象(最终返回的就是new出来的实例对象,因此this指向实例对象) 102 | 103 | #### 原型 104 | 105 | ##### 原型特性 106 | 107 | ``` javascript 108 | // Person类(构造函数、Function类的实例对象、函数) 109 | function Person(name) { 110 | this.name = name 111 | } 112 | // Person类是一个Function类的实例 true 113 | // Person继承自Function 114 | console.log(Person instanceof Function) 115 | ``` 116 | 117 | 只要创建一个类(构造函数、Function类的实例对象、函数),就会为类创建一个prototype属性,这个属性指向类的原型对象,在默认情况下,原型对象会自动获得一个constructor(构造函数)属性,这个属性对应类本身(构造函数) 118 | 119 | ```javascript 120 | console.log(Person.prototype.constructor === Person) 121 | ``` 122 | 123 | 类的所有实例共享一个原型对象,如果实例对象的方法可以通用,可以通过原型对象共享方法,而不需要为每一个实例对象开辟一个需要使用的实例方法。 124 | 125 | ``` javascript 126 | // 原型对象的方法 127 | Person.prototype.getName = function() { 128 | return this.name 129 | } 130 | // Person类的实例对象 131 | var person = new Person('ziyi2') 132 | var person1 = new Person('ziyi1') 133 | // 两个实例对象引用的是同一个原型方法 true 134 | console.log(person1.getName === person.getName) 135 | ``` 136 | 137 | 当调用构造函数创建新实例后,该实例内部将包含一个指向构造函数原型对象的[[Prototype]] 内部属性,脚本中没有标准的方式访问[[Prototype]],但在一些浏览器诸如Firefox、Safari、Chrome在每个对象上都支持属性__proto__,这个引用存在于实例与构造函数的原型对象之间,调用构造函数创建的实例都有[[Prototype]]属性,但是无法访问,可以通过isPrototypeOf()方法来确定原型对象是否对应当前实例对象 138 | 139 | ``` javascript 140 | // true 141 | console.log(person.__proto__ === Person.prototype) 142 | // true 143 | console.log(Person.prototype.isPrototypeOf(person)) 144 | ``` 145 | 146 | 读取原型链的方法和属性时,会向上遍历搜索,首先搜索实例对象本身有没有同名属性和方法,有则返回,如果没有,则继续搜索实例对象对应的原型对象的方法和属性。 147 | 148 | ```javascript 149 | 150 | // Person类的原型对象的属性 151 | // 需要注意原型对象具有动态性 152 | // 先创造实例对象后修改原型对象也能够立即生效 153 | Person.prototype.age = 28 154 | // 获取原型对象的属性 28 155 | console.log(person.age) 156 | // 检测属性是否存在于实例对象中 false 157 | console.log(person.hasOwnProperty('age')) 158 | // 给实例对象赋值属性 159 | person.age = 11 160 | // 获取了实例对象的属性 11 161 | console.log(person.age) 162 | // 检测属性是否存在于实例对象中 true 163 | console.log(person.hasOwnProperty('age')) 164 | // 删除实例对象的属性 165 | delete person.age 166 | // 仍然可以重新获取原型对象的属性 28 167 | console.log(person.age) 168 | // 检测属性是否存在于实例对象中 false 169 | console.log(person.hasOwnProperty('age')) 170 | 171 | 172 | // 判断属性存在于实例对象还是原型对象(也可能属性不存在) 173 | function hasPrototypePrpperty(obj,property){ 174 | // in可以检测到实例对象和原型对象中的属性 175 | return !obj.hasOwnProperty(property) && (property in obj) 176 | } 177 | 178 | // for...in循环 179 | // 返回所有能够通过对象访问、可枚举的(enumerated)属性,包括实例和原型对象的中的属性 180 | for(let key in person) { 181 | // name、age 182 | // name是实例对象的属性 183 | // age是原型对象的属性 184 | console.log(key) 185 | } 186 | 187 | // Object.keys() 188 | // 返回所有可枚举的实例对象的属性 189 | // ['name'] 190 | console.log(Object.keys(person)) 191 | ``` 192 | 193 | 创建类的时候默认会给类创建一个prototype属性,是类的原型对象的引用,也可以重写改类的原型对象 194 | 195 | ``` javascript 196 | function Person(name) { 197 | this.name = name 198 | } 199 | 200 | Person.prototype = { 201 | getName: function() { 202 | return this.name 203 | } 204 | } 205 | 206 | let person = new Person('ziyi2') 207 | // 此时Person类原型对象是一个新的对象 208 | // 注意和Person.prototype.getName的区别 209 | // 这个新的对象的constructor属性对应Object类 210 | console.log(Person.prototype.constructor === Object) 211 | 212 | 213 | Person.prototype = { 214 | // 使新的原型对象的constructor属性对应Person类 215 | constructor: Person, 216 | getName: function() { 217 | return this.name 218 | } 219 | } 220 | 221 | // true 222 | console.log(Person.prototype.constructor === Person) 223 | 224 | // 注意未重写原型对象之前的实例仍然指向未重写前的原型对象 225 | for(let key in person) { 226 | // name,getName 227 | console.log(key) 228 | } 229 | 230 | let person1 = new Person('ziyi1') 231 | 232 | // 重写的原型对象的constructor属性变成了可枚举属性 233 | for(let key in person1) { 234 | // name,constructor,getName 235 | console.log(key) 236 | } 237 | 238 | // 将constructor属性配置成不可枚举属性 239 | Object.defineProperty(Person.prototype,"constructor",{ 240 | enumerable:false, 241 | value:Person 242 | }) 243 | 244 | for(let key in person1) { 245 | // name,getName 246 | console.log(key) 247 | } 248 | ``` 249 | 250 | ##### 原型的弊端 251 | 252 | 253 | 原型对象的基本类型数据的属性(存放的是具体的值,因此每个实例对象的该属性值的改变互不影响)的共享对于实例对象而言非常便捷有效,但是原型对象的引用类型属性不同,原型对象的引用类型的属性存放的是一个指针(C语言中的指针的意思,指针存放的是一个地址,并不是存放一个具体的值,因为类似数组等值在一个32bit的物理块中是放不下的,肯定是放在一个连续的物理块中,因此需要一个地址去读取这些连续的物理块),指针最终指向的是一个连续的物理块,因此通过原型对象的引用类型修改的值都是改变这个物理块中的值,因此所有实例对象的该指向都会发生变化。 254 | 255 | 256 | 257 | ``` javascript 258 | function Person(name) { 259 | this.name = name 260 | } 261 | 262 | Person.prototype = { 263 | constructor: Person, 264 | getName: function() { 265 | return this.name 266 | }, 267 | age: 28, 268 | names: ['ziyi', 'ziyi1', 'ziyi2'] 269 | } 270 | 271 | var person = new Person() 272 | person.names[0] = 'ziyi_modify' 273 | person.age = 11 274 | var person1 = new Person() 275 | // ["ziyi_modify", "ziyi1", "ziyi2"] 276 | console.log(person1.names) 277 | // 28 278 | console.log(person1.age) 279 | ``` 280 | 281 | ##### 组合构造函数和原型模式 282 | 283 | 构造函数定义实例属性,原型对象定义共享的方法和基本数据类型的属性 284 | 285 | ``` javascript 286 | function Person(name) { 287 | this.name = name 288 | this.names = ['ziyi', 'ziyi1', 'ziyi2'] 289 | } 290 | 291 | Person.prototype = { 292 | constructor: Person, 293 | getName: function() { 294 | return this.name 295 | } 296 | } 297 | 298 | var person = new Person() 299 | person.names[0] = 'ziyi_modify' 300 | var person1 = new Person() 301 | // ['ziyi', 'ziyi1', 'ziyi2'] 302 | console.log(person1.names) 303 | ``` 304 | 305 | #### 继承 306 | 307 | 继承分为接口继承和实现继承,ECMAScript只支持实现继承,实现继承主要依靠原型链。 308 | 309 | ##### 原型链 310 | 311 | 假设有两个类(Son类和Father类),Son类对应一个原型对象,通过Son类创建的实例对象都包含一个指向Son类原型对象的内部指针[[Prototype]](大部分浏览器支持实例对象的__proto__属性访问原型对象)。假如让Son类的原型对象引用Father类的实例对象,则Son类的原型对象将包含一个指向Father类的原型对象的内部指针[[Prototype]],从而使Son类的原型对象可以共享Father类原型对象和实例对象(注意这里也包括实例对象)的方法和属性,从而又使Son类的实例对象可以共享Father类原型对象的方法和属性,这就是原型链。 312 | 313 | 原型链实现了方法和属性的继承,此时Son类是子类,继承了Father类这个父类。 314 | 315 | ``` javascript 316 | function Father() { 317 | this.names = ['ziyi','ziyi1', 'ziyi2'] 318 | } 319 | function Son() {} 320 | 321 | Father.prototype.getName = function() { 322 | return Father.name 323 | } 324 | 325 | // Son类的原型对象包含了Father类原型对象的方法和属性 326 | // 同时也包含了Father类实例对象的实例方法和实例属性 327 | Son.prototype = new Father() 328 | 329 | // 重写了Son类的原型对象 330 | // Son.prototype.constructor !== Son 331 | // true 332 | console.log(Son.prototype.constructor === Father) 333 | 334 | let son = new Son() 335 | 336 | // 继承Father类实例对象的引用类型属性 337 | // ["ziyi", "ziyi1", "ziyi2"] 338 | console.log(son.names) 339 | 340 | // 继承Father类原型对象的方法 341 | console.log(son.getName()) 342 | ``` 343 | 344 | > 读取对象的属性和方法时,会执行搜索,首先搜索实例对象本身有没有同名的属性和方法,有则返回, 如果没有找到,那么继续搜索原型对象,在原型对象中查找具有给定名字的属性和方法。 345 | 346 | 实现原型链,本质上就是扩展了原型搜索机制 347 | 348 | - 搜索实例 349 | - 搜索实例的原型(该原型同时也是另一个类的实例) 350 | - 搜索实例的原型的原型 351 | - ... 352 | 353 | 一直向上搜索,直到第一次搜索到属性或者方法为止,搜索不到,到原型链的末端停止。 354 | 355 | 356 | 该继承方法有一个缺陷,具体可以查看原型的弊端,在原型中使用引用类型的属性,在所有的实例对象中的该属性都引用了同一个物理空间,一旦空间的值发生了变化,那么所有实例对象的该属性值就发生了变化。 357 | 358 | ``` javascript 359 | // son实例对象修改Father类实例对象(Son类的原型对象)的引用类型属性 360 | son.names.push('ziyi3') 361 | let son1 = new Son() 362 | // son1中仍然引用的是Father类实例对象(Son类的原型对象)的引用类型属性 363 | // ["ziyi", "ziyi1", "ziyi2", "ziyi3"] 364 | console.log(son1.names) 365 | ``` 366 | 367 | ##### 借用构造函数(伪造对象或经典继承) 368 | 369 | 为了避开原型中有引用类型数据的问题,做到子类继承(这里的继承只是创建和父类相同的实例对象的属性和方法)父类的实例对象的实例方法和实例属性,在子类的构造函数中调用父类的构造函数,从而使子类的this对象在父类构造函数中执行,并最终返回的是子类的this对象(我们知道子类的this对象在构造函数的执行过程中都是开辟新的对象空间,因此引用类型的实例属性都是不同的指针地址)。 370 | 371 | ``` javascript 372 | function Father() { 373 | this.names = ['ziyi','ziyi1', 'ziyi2'] 374 | } 375 | function Son() { 376 | // 使用new关键字执行的构造函数this指向实例对象 377 | // 注意如果不用new关键字执行this指向全局对象window 378 | // 这里的Father类当做一个普通的执行函数 379 | // 此时只是让Son类的实例对象创建了和Father类实例对象一样的实例属性和实例方法 380 | Father.call(this) 381 | 382 | // Father.call(this)类似于在Son构造函数中执行 383 | // this.names = ['ziyi','ziyi1', 'ziyi2'] 384 | } 385 | 386 | let son = new Son() 387 | son.names.push('ziyi3') 388 | // ["ziyi", "ziyi1", "ziyi2", "ziyi3"] 389 | console.log(son.names) 390 | 391 | let son1 = new Son() 392 | // ['ziyi','ziyi1', 'ziyi2'] 393 | console.log(son1.names) 394 | ``` 395 | 396 | 如果此时父类有自己的实例属性,而子类也有自己的实例属性 397 | 398 | ``` javascript 399 | function Father(name,age) { 400 | this.name = name 401 | this.age = age 402 | } 403 | function Son() { 404 | Father.apply(this, arguments) 405 | this.job = arguments[2] 406 | } 407 | 408 | let son = new Son('ziyi2', 28, 'web') 409 | // {name: "ziyi2", age: 28, job: "web"} 410 | console.log(son) 411 | ``` 412 | 413 | ##### 组合继承 414 | 415 | 为了防止原型对象引用类型在实例对象中是同一个指针的问题,在原型链的实现中可以混合借用构造函数实现组合继承 416 | 417 | ``` javascript 418 | function Father(name,age) { 419 | this.name = name 420 | this.age = age 421 | // 引用类型的实例属性 422 | this.names = [] 423 | } 424 | 425 | Father.prototype.getNames = function() { 426 | return this.names 427 | } 428 | 429 | function Son(name, age, job) { 430 | // 借用构造函数 431 | // this执行的过程中也会创建this.names实例属性 432 | Father.call(this, name, age) 433 | this.job = job 434 | } 435 | 436 | // 创建原型链 437 | // 需要注意此时Son类的原型对象中还是有Father类的实例对象的属性和方法 438 | Son.prototype = new Father() 439 | // 调整Son原型对象的constructor指向 440 | Son.prototype.constructor = Son 441 | 442 | let son = new Son('ziyi2', 28, 'web') 443 | son.names.push('ziyi2') 444 | // { age:28 job:"web" name:"ziyi2" names:["ziyi2"] } 445 | console.log(son) 446 | let son1 = new Son('ziyi2', 28, 'web') 447 | // [] son.name和son1.names是不同的指针,指向不同的物理空间 448 | console.log(son1.names) 449 | ``` 450 | 451 | > 需要注意的是Son类的实例对象中有name、age和names属性,Son类的原型对象中也有这些属性,只是根绝原型链的搜索机制,在使用实例对象访问这些属性时,首先搜索了实例对象中的该同名属性,因此原型对象中的该属性被屏蔽。 452 | 453 | ``` javascript 454 | // undefined 455 | console.log(Son.prototype.age) 456 | // undefined 457 | console.log(Son.prototype.name) 458 | // [] 459 | console.log(Son.prototype.names) 460 | ``` 461 | 462 | ##### 寄生组合式继承 463 | 464 | | 类型 | 优缺点 | 465 | | :-------- | :--------| 466 | | 构造函数模式 | 可以创建不同实例属性的副本,包括引用类型的实例属性,但是不能共享方法 | 467 | | 原型模式 | 引用类型的属性对于实例对象而言共享同一个物理空间,因此可以共享方法 | 468 | | 原型链 | 对父类实现方法和属性继承的过程中,父类实例对象的引用类型属性在子类的实例中共享同一个物理空间,因为父类的实例对象指向了子类的原型对象 | 469 | | 借用构造函数 | 解决了继承中的引用值类型共享物理空间的问题,但是没法实现方法的共享 | 470 | | 组合继承 | 属性的继承使用借用构造函数方法,方法的继承使用原型链技术,即解决了引用值类型共享的问题,又实现了方法的共享,但是子类的原型对象中还存在父类实例对象的实例属性 | 471 | 472 | 目前而言,组合继承已经可以解决大部分问题,但是也有缺陷,就是会调用两次父类的构造函数,一次是实现原型时使子类的原型等于父类的实例对象调用了父类构造函数(同时在子类的原型对象中还存在了父类实例对象的实例属性),一次是使用子类构造函数时调用了一次父类构造函数。 473 | 474 | 寄生组合式继承可以解决在继承的过程中子类的原型对象中还存在父类实例对象的实例属性的问题。 475 | 476 | ``` javascript 477 | // 继承o原型对象的方法和属性 478 | // 需要注意o的引用类型属性在F实例对象中共享同一个物理空间(需要避免使用引用类型属性值) 479 | // 当然o的方法在F实例对象中共享 480 | function objectCreate(o) { 481 | // F类没有实例方法和实例属性 482 | function F() {} 483 | F.prototype = o 484 | // o是F类的原型对象 485 | // 返回F类的实例对象 486 | // new F().__proto__ = o 487 | // F类实例对象共享o的属性和方法 488 | // true 489 | console.log(new F().__proto__ === o) 490 | return new F() 491 | } 492 | 493 | 494 | // SubClass实现对于SuperClass原型对象的方法和属性的继承 495 | function inheritPrototype(SubClass, SuperClass) { 496 | // prototype是一个实例对象 497 | // prototype不是SuperClass的实例对象而是另一个类F类的实例对象 498 | // SuperClass类和objectCreate函数中的F类的原型对象都是SuperClass.prototype 499 | // 这里没有调用SuperClass构造函数 500 | // prototype继承了SuperClass类的原型对象的方法和属性 501 | var prototype = objectCreate(SuperClass.prototype) 502 | // 使prototype实例对象的constructor属性指向SubClass子类 503 | // 因为重写SuperClass的原型对象时会修改constructor属性 504 | // SubClass.prototype.constructor = SubClass 505 | prototype.constructor = SubClass 506 | // 使SubClass类的原型对象指向prototype实例对象 507 | // 类似于SubClass.prototype = new SuperClass() 508 | // 只是这里是SubClass.prototype = new F() 509 | // F类本身没有实例方法和实例属性 510 | SubClass.prototype = prototype 511 | } 512 | 513 | 514 | 515 | function Father(name, age) { 516 | this.name = name 517 | this.age = age 518 | } 519 | 520 | Father.prototype.getName = function() { 521 | return this.name 522 | } 523 | 524 | function Son1(name, age, job) { 525 | // 借用构造函数 526 | Father.apply(this, arguments) 527 | this.job = job 528 | } 529 | 530 | function Son2(name, age, job) { 531 | // 借用构造函数 532 | Father.apply(this, arguments) 533 | this.job = job 534 | } 535 | 536 | 537 | 538 | // 组合继承的写法 539 | Son1.prototype = new Father() 540 | Son1.prototype.constructor = Son1 541 | // age : undefined 542 | // constructor : ƒ Son1(name, age, job) 543 | // name : undefined 544 | // __proto__ : { getName:ƒ () constructor : ƒ Father(name, age) } 545 | console.log(Son1.prototype) 546 | 547 | // 寄生组合式继承的写法 548 | // 借用构造函数实现构造函数的方法和属性的继承 549 | // inheritPrototype函数实现原型对象的方法和属性的继承 550 | inheritPrototype(Son2, Father) 551 | // constructor : ƒ Son2(name, age, job) 552 | // __proto__ : { getName:ƒ () constructor : ƒ Father(name, age) } 553 | console.log(Son2.prototype) 554 | 555 | 556 | var son1 = new Son1('ziyi2', 28, 'web') 557 | var son2 = new Son2('ziyi3', 28, 'hardware') 558 | son1.getName() 559 | son2.getName() 560 | ``` 561 | 562 | ### ES6中类的继承 563 | 564 | #### 类 565 | 566 | ES6中的类只是ES5封装后的语法糖而已 567 | 568 | ``` javascript 569 | // ES6中实现的Person类 570 | class Es6Person { 571 | constructor(name, age) { 572 | // 实例对象的属性 573 | this.name = name 574 | this.age = age 575 | // 实例对象的方法 576 | this.getName = () => { 577 | return this.name 578 | } 579 | } 580 | 581 | // Person类原型对象的方法 582 | getAge() { 583 | return this.age 584 | } 585 | } 586 | 587 | 588 | // ES5中实现的Person类 589 | function Es5Person (name, age) { 590 | // 实例对象的属性 591 | this.name = name 592 | this.age = age 593 | // 实例对象的方法 594 | this.getName = () => { 595 | return this.name 596 | } 597 | } 598 | 599 | // Person类原型对象的方法 600 | Es5Person.prototype.getAge = function() { 601 | return this.age 602 | } 603 | ``` 604 | 605 | 在ES5中类的原型对象的方法是可枚举的,但是ES6中不可枚举 606 | 607 | ``` javascript 608 | // [] 609 | console.log(Object.keys(Es6Person.prototype)) 610 | // ["getAge"] 611 | console.log(Object.keys(Es5Person.prototype)) 612 | ``` 613 | 614 | 615 | 在ES5中如果不用new,this指向windows全局变量,在ES6如果不用new关键字则会报错处理 616 | 617 | ``` javascript 618 | // Uncaught TypeError: Class constructor Person cannot be invoked without 'new' 619 | let person2 = Es6Person('ziyi2', 111) 620 | ``` 621 | 622 | 623 | ES6中的类是不会声明提升的, ES5可以 624 | 625 | ``` javascript 626 | // Es5Person {} 627 | console.log(Es5Person) 628 | // Uncaught ReferenceError: Es6Person is not defined 629 | let es6 = new Es6Person() 630 | console.log(es6) 631 | 632 | class Es6Person {} 633 | function Es5Person {} 634 | ``` 635 | 636 | 637 | 在ES6中如果不写构造方法 638 | 639 | ``` javascript 640 | class Es6Person {} 641 | 642 | // 等同于 643 | class Es6Person { 644 | constructor() {} 645 | } 646 | ``` 647 | 648 | 649 | 在ES6中类的属性名可以采用表达式 650 | 651 | ``` javascript 652 | const getAge = Symbol('getAge') 653 | 654 | class Es6Person { 655 | constructor(name, age) { 656 | this.name = name 657 | this.age = age 658 | this.getName = () => { 659 | return this.name 660 | } 661 | } 662 | 663 | // 表达式 664 | [getAge]() { 665 | return this.age 666 | } 667 | } 668 | 669 | let es6Person = new Es6Person('ziyi2', 28) 670 | es6Person[getAge]() 671 | ``` 672 | 673 | #### 类的私有方法和私有属性 674 | 675 | 在ES6的类中目前并没有私有方法和私有属性的标准语法,但是可以通过其他方式模拟实现,例如属性名采用表达式,而ES5中很难实现 676 | 677 | ``` javascript 678 | // 利用Symbol值的唯一性 679 | const getAge = Symbol('getAge') 680 | const job = Symbol('job') 681 | 682 | class Es6Person { 683 | constructor(name, age) { 684 | this.name = name 685 | this.age = age 686 | this.getName = () => { 687 | return this.name 688 | } 689 | // 私有属性 690 | this[job] = 'web' 691 | } 692 | 693 | // 私有方法 694 | [getAge]() { 695 | return this.age 696 | } 697 | } 698 | 699 | let es6Person = new Es6Person('ziyi2', 28) 700 | es6Person[getAge]() 701 | es6Person[job] 702 | ``` 703 | 704 | > 此时如果Es6Person类处在一个文件中,那么getAge和job变量是当前文件的局部变量,外部文件无法访问,从而在外部文件调用Es6Person类的时候无法访问具有唯一性的getAge和job变量,从而使之成为私有方法和私有属性。目前私有属性有一个3阶段的[提案](https://github.com/tc39/proposal-private-methods)。 705 | 706 | 707 | #### 类的getter和setter 708 | 709 | ES6利用get和set关键字对某属性设置存值函数和取值函数拦截属性的存取行为和ES5一样。 710 | 711 | ``` javascript 712 | class Es6Person { 713 | constructor(name,age) { 714 | this.name = name 715 | this.age = age 716 | } 717 | 718 | get name() { 719 | return 'ziyi2' 720 | } 721 | 722 | set name(value) { 723 | console.log('setter: ', value) 724 | } 725 | } 726 | 727 | // setter: ziyi3 728 | let person = new Es6Person('ziyi3', 11) 729 | // ziyi2 730 | console.log(person.name) 731 | // setter: ziyi2 732 | person.name = 'ziyi2' 733 | ``` 734 | 735 | #### 类的Generator方法 736 | 737 | 给类增加一个遍历器,从而使类可以进行for...of操作 738 | 739 | ``` javascript 740 | class Es6Person { 741 | constructor(...args) { 742 | this.args = args 743 | } 744 | 745 | * [Symbol.iterator]() { 746 | for(let arg of this.args) { 747 | yield arg 748 | } 749 | } 750 | } 751 | 752 | let person = new Es6Person('ziyi3', 11) 753 | 754 | for(let value of person) { 755 | // ziyi3 756 | // 11 757 | console.log(value) 758 | } 759 | ``` 760 | 761 | #### 类的静态方法 762 | 763 | ``` javascript 764 | class Es6Person { 765 | // 类的静态方法 766 | static getClassName() { 767 | // this指向类,而不是实例对象 768 | return 'Es6Person' 769 | } 770 | 771 | // 类的静态方法 772 | static getName() { 773 | // this指向类,而不是实例对象 774 | return this.getClassName() 775 | } 776 | } 777 | 778 | // Es6Person 779 | console.log(Es6Person.getName()) 780 | ``` 781 | 782 | 783 | #### 类的静态属性提案 784 | 785 | ES6明确规定,Class内部只有静态方法,没有静态属性,目前有一个静态属性的提案,对静态属性规定了新的写法 786 | 787 | ``` javascript 788 | class Es6Person { 789 | static className = 'Es6Person' 790 | } 791 | ``` 792 | 793 | #### new.target 794 | 795 | new.target返回new命令作用于的那个构造函数 796 | 797 | 798 | ``` javascript 799 | class Es6Person { 800 | constructor(name) { 801 | // new.target = class Es6Person {} 802 | if(new.target === Es6Person) { 803 | this.name = name 804 | } else { 805 | throw new Error('没有用new命令生成实例对象') 806 | } 807 | } 808 | } 809 | let person = new Es6Person('ziyi2') 810 | ``` 811 | 812 | > new.target可以用于指定哪些子类可以继承父类。 813 | 814 | #### 类的继承 815 | 816 | ES5 的继承使用借助构造函数实现,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面。ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。 817 | 818 | 819 | 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象 820 | 821 | ``` javascript 822 | class Es6Person { 823 | constructor(name) { 824 | this.name = name 825 | } 826 | } 827 | 828 | 829 | class Es6WebDeveloper extends Es6Person { 830 | constructor(name, age) { 831 | // 表示父类构造函数 832 | // 子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例 833 | super(name) 834 | this.age = age 835 | } 836 | } 837 | ``` 838 | 839 | 例如以下写法会报错 840 | 841 | ``` javascript 842 | class Es6Person { 843 | constructor(name) { 844 | this.name = name 845 | } 846 | } 847 | 848 | 849 | class Es6WebDeveloper extends Es6Person { 850 | constructor(name, age) { 851 | // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor 852 | console.log(this) 853 | super(name) 854 | } 855 | } 856 | 857 | let developer = new Es6WebDeveloper('ziyi2', 11) 858 | 859 | ``` 860 | 861 | 此时的继承类似于ES5的寄生组合式继承,子类的原型对象中没有父类的实例对象和实例方法 862 | 863 | ``` javascript 864 | // Es6Person {constructor: ƒ} 865 | console.log(Es6WebDeveloper.prototype) 866 | ``` 867 | 868 | ES6在继承的语法上显然也考虑的更加周到,不仅继承了类的原型对象,还继承了类的静态属性和静态方法 869 | 870 | ``` javascript 871 | class Es6Person { 872 | 873 | static getClassName() { 874 | return Es6Person.name 875 | } 876 | 877 | constructor(name) { 878 | this.name = name 879 | } 880 | } 881 | 882 | 883 | class Es6WebDeveloper extends Es6Person { 884 | constructor(name, age) { 885 | super(name) 886 | this.age = age 887 | } 888 | } 889 | 890 | // Es6Person 891 | Es6WebDeveloper.getClassName() 892 | ``` 893 | 894 | 895 | #### super关键字 896 | 897 | super作为函数调用时,代表父类的构造函数,只能在子类的构造函数中使用 898 | 899 | ``` javascript 900 | class Es6Person { 901 | constructor(name) { 902 | // Es6WebDeveloper 903 | console.log(new.target.name) 904 | // Es6WebDeveloper {} 905 | console.log(this) 906 | this.name = name 907 | } 908 | } 909 | 910 | class Es6WebDeveloper extends Es6Person { 911 | constructor(name, age) { 912 | // 类似于A.prototype.constructor.call(this) 913 | // super内部的this指向Es6WebDeveloper 914 | super(name) 915 | this.age = age 916 | } 917 | } 918 | 919 | let developer = new Es6WebDeveloper('ziyi2') 920 | ``` 921 | 922 | 除此之外,super当做一个对象使用,在子类普通方法(原型对象)中,指向父类的原型对象,因此可以调用父类的原型方法,需要注意的是执行父类的原型方法时,在方法中执行时this指向的是子类的实例对象而不是父类的实例对象 923 | 924 | ``` javascript 925 | class Es6Person { 926 | constructor(name) { 927 | this.name = name 928 | } 929 | getName() { 930 | // Es6WebDeveloper {name: "ziyi2", age: undefined} 931 | // 子类的whoIsSuper中调用的super.getName在这里执行的this指向子类实例对象 932 | console.log(this) 933 | return this.name 934 | } 935 | } 936 | 937 | Es6Person.prototype.className = Es6Person.name 938 | 939 | class Es6WebDeveloper extends Es6Person { 940 | constructor(name, age) { 941 | super(name) 942 | this.age = age 943 | } 944 | whoIsSuper() { 945 | // true 946 | console.log(super.getName === Es6Person.prototype.getName) 947 | 948 | // super无法获取父类的实例属性 949 | // 因为super指向的是父类的原型对象 950 | 951 | // undefined 952 | console.log(super.name) 953 | // Es6Person 954 | console.log(super.className) 955 | 956 | return super.getName() 957 | } 958 | } 959 | 960 | let developer = new Es6WebDeveloper('ziyi2') 961 | // ziyi2 962 | console.log(developer.whoIsSuper()) 963 | ``` 964 | 965 | 如果super用作对象且不在子类的原型对象中调用,而是在子类的静态方法中调用,那么super指代父类而不是父类的原型对象,同理调用父类静态方法时this指向子类而不是父类 966 | 967 | ``` javascript 968 | class Es6Person { 969 | static getClassName() { 970 | // class Es6WebDeveloper extends Es6Person { 971 | // ... 972 | // 在子类中使用super调用时,this指向子类 973 | console.log(this) 974 | return this.name 975 | } 976 | 977 | constructor(name) { 978 | this.name = name 979 | } 980 | } 981 | 982 | class Es6WebDeveloper extends Es6Person { 983 | constructor(name, age) { 984 | super(name) 985 | this.age = age 986 | } 987 | 988 | static getClassName() { 989 | return super.getClassName() 990 | } 991 | } 992 | 993 | // Es6WebDeveloper 994 | console.log(Es6WebDeveloper.getClassName()) 995 | ``` 996 | 997 | #### ES6的继承原理 998 | 999 | __proto__存在于实例与构造函数的原型对象之间 1000 | 1001 | ``` javascript 1002 | class Es6Person { 1003 | constructor(name) { 1004 | this.name = name 1005 | } 1006 | } 1007 | 1008 | let es6Person = new Es6Person('ziyi2') 1009 | // true 1010 | console.log(es6Person.__proto__ === Es6Person.prototype) 1011 | ``` 1012 | 1013 | 1014 | ES6的继承原理 1015 | 1016 | ``` javascript 1017 | class Es6Person {} 1018 | class Es6WebDeveloper extends Es6Person {} 1019 | 1020 | // true 1021 | // Es6WebDeveloper看做一个实例对象 1022 | // Es6Person看做一个原型对象 1023 | // 因此Es6WebDeveloper继承了Es6Person的所有属性和方法 1024 | // 实现了类的静态属性和方法的继承 1025 | // 子类的原型是父类 1026 | console.log(Es6WebDeveloper.__proto__ === Es6Person) 1027 | 1028 | // true 1029 | // 这里类似于 Es6WebDeveloper.prototype = new Es6Person() 1030 | // 和ES5的原型链一样 1031 | // 子类的原型对象是父类的原型对象的实例 1032 | // 子类的实例继承父类的实例 1033 | console.log(Es6WebDeveloper.prototype.__proto__ === Es6Person.prototype) 1034 | ``` 1035 | 1036 | 因此以下继承都是可理解的 1037 | 1038 | ``` javascript 1039 | class A extends Object { 1040 | } 1041 | 1042 | A.__proto__ === Object // true 1043 | A.prototype.__proto__ === Object.prototype // true 1044 | 1045 | 1046 | class A { 1047 | } 1048 | 1049 | A.__proto__ === Function.prototype // true 1050 | A.prototype.__proto__ === Object.prototype // true 1051 | ``` 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | -------------------------------------------------------------------------------- /JS设计模式.md: -------------------------------------------------------------------------------- 1 | # 设计模式 2 | 3 | ## 概念 4 | 5 | ### 什么是模式 6 | 7 | 模式是一种可复用的解决方案,可用于解决软件设计中遇到的常见问题。 8 | 9 | ### 模式的优点 10 | 11 | - 复用模式有助于防止在应用程序开发过程中小问题引发大问题 12 | - 模式可以提供通用的解决方案 13 | - 某些模式可以通过避免代码复用减少代码的总体资源占用量 14 | - 模式会使开发沟通更快速 15 | - 模式可以逐步改进 16 | 17 | ### 设计模式的结构 18 | 19 | - 模式名称 20 | - 描述 21 | - 上下文大纲 22 | - 问题陈述 23 | - 解决方案 24 | - 设计 25 | - 实现 26 | - 插图 27 | - 示例 28 | - 辅助条件 29 | - 关系 30 | - 已知的用法 31 | - 讨论 32 | 33 | 34 | 35 | ### 反模式 36 | 37 | JavaScript中的反模式示例如下 38 | 39 | - 在全局上下文中定义大量的变量污染全局命名空间 40 | - 向setTimeout或setInterval传递字符串,而不是函数 41 | - 修改Object类的原型 42 | - 以内联形式使用JavaScript 43 | - 使用document.write 44 | 45 | ### 设计模式的类别 46 | 47 | #### 创建型 48 | 49 | 专注于处理对象创建机制,以适合给定情况的方式来创建对象。创建对象的基本方法可能导致项目复杂性增加,而创建型模式旨在通过控制创建过程来解决这种问题。 50 | 51 | | 设计模式 | 描述 | 52 | | :--------| :------: | 53 | | Constructor(构造器) | | 54 | | Factory(工厂) | | 55 | | Abstract(抽象) | | 56 | | Prototype(原型) | | 57 | | Singleton(单例) | | 58 | | Singleton(单例) | | 59 | | Builder(生成器) | | 60 | 61 | #### 结构型 62 | 63 | 结构型与对象组合有关,通常可以用于找出在不同对象之间建立关系的简单方法。这种模式有助于确保在系统某一部分发生变化时,系统的整个结构不需要同时改变,同时对于不适合某一特定目的而改变的系统部分,也能够完成重组。 64 | 65 | | 设计模式 | 描述 | 66 | | :--------| :------: | 67 | | Iterator(装饰者) | | 68 | | Facade(外观) | | 69 | | Flyweight(享元) | | 70 | | Adapter(适配器) | | 71 | | Proxy(代理) | | 72 | 73 | #### 行为 74 | 75 | 行为设计模式专注于改善或简化系统中不同对象之间的通信。 76 | 77 | | 设计模式 | 描述 | 78 | | :--------| :------: | 79 | | Iterator(迭代器) | | 80 | | Mediator(中介者) | | 81 | |Observer(观察者) | | 82 | |Visitor(访问者) | | 83 | 84 | ## Constructor(构造器)模式 85 | 86 | ``` javascript 87 | function Person() {} 88 | var person = new Person() 89 | 90 | // 带原型的Constructor 91 | Person.prototype.getName = {} 92 | ``` 93 | 94 | ## Module(模块)模式 95 | 96 | - 对象字面量表示法 97 | - Module模式 98 | - AMD模式 99 | - CommonJS模块 100 | - ECMAScript Harmony模块 101 | 102 | ### 对象字面量表示法 103 | 104 | ``` javascript 105 | let person = { 106 | name: '', 107 | age: 0, 108 | getName: function() {}, 109 | getAge: function() {} 110 | } 111 | ``` 112 | 113 | ### Module模式 114 | 115 | Module模式可以为类提供私有和公有的方法,Module模式会封装一个作用域,从而屏蔽来自全局作用域的特殊部分,使一个单独的对象拥有公有/私有的方法和变量。 116 | 117 | Module模式使用闭包封装私有状态和组织,提供一种包装混合公有/私有方法和变量的方式,防止其泄露至全局作用域,防止与全局作用域中的方法和变量发生冲突。通过该模式只需返回一个公有API,而其他的一切则都维持在私有闭包里。 118 | 119 | Module模式可以屏蔽处理底层事件逻辑,只暴露供应用程序调用的公有API,该模式返回的是一个对象而不是函数。 120 | 121 | ``` javascript 122 | // 一个立即执行的匿名函数创建了一个作用域 123 | // 全局作用域无法获取私有变量_counter 124 | var moduleMode = (function() { 125 | // 私有变量 126 | var _counter = 0 127 | 128 | // 返回一个公有对象 129 | return { 130 | // 公有API 131 | increment: function() { 132 | return ++_counter 133 | }, 134 | // 公有API 135 | reset: function() { 136 | _counter = 0 137 | return _counter 138 | } 139 | } 140 | })() 141 | 142 | let counter = moduleMode.increment() 143 | console.log(counter) 144 | counter = moduleMode.increment() 145 | console.log(counter) 146 | counter = moduleMode.reset() 147 | console.log(counter) 148 | // _counter is not defined 149 | console.log(_counter) 150 | ``` 151 | 152 | > Module模式的本质是使用函数作用域来模拟私有变量,在模式内,由于闭包的存在,声明的变量和方法旨在改模式内部可用,但在返回对象上定义的变量和方法,则对外部使用者可用。 153 | 154 | Module模式也可用于命名空间 155 | 156 | ``` javascript 157 | var Namespace = (function() { 158 | // 私有变量 159 | var _counter = 0 160 | 161 | // 私有方法 162 | var _sayCounter = function() { 163 | console.log(_counter) 164 | } 165 | 166 | // 返回一个公有对象 167 | return { 168 | // 公有变量 169 | counter: 10, 170 | 171 | // 公有API 172 | increment: function() { 173 | ++_counter 174 | // 调用私有变量 175 | _sayCounter() 176 | return _counter 177 | }, 178 | // 公有API 179 | reset: function() { 180 | _counter = 0 181 | _sayCounter() 182 | return _counter 183 | } 184 | } 185 | })() 186 | 187 | Namespace.increment() 188 | Namespace.increment() 189 | Namespace.reset() 190 | ``` 191 | 192 | ### Module模式的变化 193 | 194 | #### 引入 195 | 196 | 可以使jQuery、Underscore作为参数引入模块 197 | 198 | ``` javascript 199 | var moduleMode = (function($, _) { 200 | function _method() { 201 | $('.container').html('test') 202 | } 203 | 204 | return { 205 | method: function() { 206 | _method() 207 | } 208 | } 209 | })($,_) 210 | 211 | moduleMode.method() 212 | ``` 213 | 214 | #### 引出 215 | 216 | ``` javascript 217 | 218 | let moduleMode = (function() { 219 | var public = {}, 220 | _private = 'hello' 221 | 222 | function _method() { 223 | console.log(_private) 224 | } 225 | 226 | public.name = 'public' 227 | 228 | public.method = function () { 229 | _method() 230 | } 231 | 232 | return public 233 | })() 234 | 235 | console.log(moduleMode.name) 236 | ``` 237 | 238 | ### Module模式的优缺点 239 | 240 | - 优点:整洁、支持私有数据。 241 | - 缺陷:私有数据难以维护(想改变可见性需要修改每一个使用该私有数据的地方),无法为私有成员创建自动化单元测试,开发人员无法轻易扩展私有方法。 242 | 243 | ## Singleton(单例/单体)模式 244 | 245 | 单例模式作为一个静态的实例实现时,可以延迟创建实例,从而在没有使用该静态实例之前,无需占用浏览器的资源或内存。同时当在系统中确实需要一个对象来协调其他对象时,Singleton模式非常有用。单例模式可以推迟初始化,通常是因为它需要一些信息,这些信息在初始化期间可能无法获得。 246 | 247 | ``` javascript 248 | var Singleton = (function() { 249 | function Single(options) { 250 | options = options || {} 251 | this.name = options.name || 'Single' 252 | this.age = options.age || 1 253 | } 254 | 255 | var _instance 256 | 257 | // 返回一个闭包,_instance不会被销毁 258 | return { 259 | name: 'Singleton', 260 | getInstance: function(options) { 261 | if(!_instance) { 262 | _instance = new Single(options) 263 | } 264 | return _instance 265 | } 266 | } 267 | })() 268 | 269 | 270 | var single = Singleton.getInstance({ 271 | name: 'ziyi2', 272 | age: 28 273 | }) 274 | 275 | // Single {name: "ziyi2", age: 28} 276 | console.log(single) 277 | ``` 278 | 279 | ## Observer(观察者)模式 280 | 281 | 观察者模式是使用一个subject目标对象维持一系列依赖于它的observer观察者对象,将有关状态的任何变更自动通知给这一系列观察者对象。当subject目标对象需要告诉观察者发生了什么事情时,它会向观察者对象们广播一个通知。 282 | > 一个或多个观察者对目标对象的状态感兴趣时,可以将自己依附在目标对象上以便注册感兴趣的目标对象的状态变化,目标对象的状态发生改变就会发送一个通知消息,调用每个观察者的更新方法。如果观察者对目标对象的状态不感兴趣,也可以将自己从中分离。 283 | 284 | 285 | | 对象 | 描述 | 286 | | :--------| :------ | 287 | | Subject(目标) | 维护一系列的观察者,方便添加、删除和通知观察者 | 288 | | Observer(观察者) | 为那些目标状态发生改变时需要通知的对象提供一个更新接口 | 289 | | subject(目标)实例对象 | 状态发生变化时通知观察者实例对象们更新状态 | 290 | |observer(观察者)实例对象 | 实现更新接口用于更新状态 | 291 | 292 | 293 | ![观察者设计模式](https://raw.githubusercontent.com/ziyi2/js/master/images/Observer.png) 294 | 295 | 296 | 297 | ### 观察者列表对象 298 | 299 | 观察者列表对象用于维护一系列的观察者实例对象 300 | 301 | ``` javascript 302 | // 观察者列表对象 303 | function ObserverList() { 304 | this.observerList = [] 305 | } 306 | 307 | // 增加观察者实例对象 308 | ObserverList.prototype.add = function(observer) { 309 | return this.observerList.push(observer) 310 | } 311 | 312 | // 查看观察者实例对象的数量 313 | ObserverList.prototype.getCount = function() { 314 | return this.observerList.length 315 | } 316 | 317 | // 获取某个观察者实例对象 318 | ObserverList.prototype.get = function(index) { 319 | if(index < -1 || index > this.observerList.length) return 320 | return this.observerList[index] 321 | } 322 | 323 | // 删改观察者实例对象的列表 省略... 324 | ``` 325 | 326 | ### Subject(目标)对象 327 | 328 | 使用观察者列表对象维护观察者实例对象,并可以通知观察者实例对象更新状态 329 | 330 | ``` javascript 331 | // 目标对象(在观察者列表上增、删观察者实例对象) 332 | function Subject() { 333 | this.observers = new ObserverList() 334 | } 335 | 336 | // 增加观察者实例对象 337 | Subject.prototype.add = function(observer) { 338 | this.observers.add(observer) 339 | } 340 | 341 | // 通知观察者列表更新 342 | Subject.prototype.notify = function(context) { 343 | var count = this.observers.getCount() 344 | for(var i=0; i 观察者实例对象或目标实例对象 367 | // extension -> 需要被扩展的对象 368 | function extend(obj, extension) { 369 | for(var key in obj) { 370 | extension[key] = obj[key] 371 | } 372 | } 373 | ``` 374 | 375 | ### 具体的DOM元素用于创建观察者实例对象和目标实例对象 376 | 377 | 创建DOM元素,用于扩展观察者实例对象和目标实例对象 378 | 379 | ``` html 380 | 381 | 382 | 383 | 384 |
385 | ``` 386 | 387 | 创建目标实例对象 388 | 389 | ``` javascript 390 | // 获取checkbox元素 391 | var checkbox = document.getElementById('checkbox') 392 | 393 | // 创建具体目标实例,并绑定到checkbox元素上 394 | extend(new Subject(), checkbox) 395 | 396 | // 点击checkbox会触发目标实例对象的通知方法 397 | // 广播到所有观察者实例对象促使它们调用更新状态方法 398 | checkbox.onclick = function() { 399 | checkbox.notify(checkbox.checked) 400 | } 401 | ``` 402 | 403 | 创建观察者实例对象 404 | 405 | ``` javascript 406 | // 获取btn和div元素 407 | var btn = document.getElementById('btn'), 408 | div = document.getElementById('div') 409 | 410 | btn.onclick = handlerClick 411 | 412 | function handlerClick() { 413 | // 创建checkbox元素(注意和目标实例对象不同) 414 | var input = document.createElement("input") 415 | input.type = "checkbox" 416 | 417 | // 创建具体的观察者实例,并绑定到checkbox元素上 418 | extend(new Observer(), input) 419 | 420 | // 重写观察者更新行为 421 | input.update = function(value) { 422 | this.checked = value 423 | } 424 | 425 | // 通过目标实例对象新增需要被广播的观察者实例对象 426 | checkbox.add(input) 427 | 428 | // 将观察者附到div元素上 429 | div.appendChild(input) 430 | } 431 | ``` 432 | 433 | > 至此,通过按钮新增观察者实例对象,点击目标checkbox实例对象时,checkbox的状态会广播给所有新增的观察者实例对象checkbox,从而使目标实例对象的值和观察者实例对象的值保持一致,实现了观察者模式。 434 | 435 | 436 | ## Publish/Subscribe(发布/订阅)模式 437 | 438 | 439 | ![发布/订阅设计模式](https://raw.githubusercontent.com/ziyi2/js/master/images/pubsub.png) 440 | 441 | 442 | 需要注意token是每一次订阅的唯一标识,通过token可以取消特定的频道订阅。 443 | 444 | 445 | 446 | ``` javascript 447 | 448 | // 发布/订阅模式 449 | var pubsub = (function() { 450 | 451 | // 订阅和发布的事件频道集(桥梁、中间带) 452 | var _channels = [], 453 | _subUid = -1 454 | 455 | return { 456 | // 订阅频道 457 | subscribe: function(channel, handler) { 458 | if(!_channels[channel]) _channels[channel] = [] 459 | var token = (++_subUid).toString() 460 | _channels[channel].push({ 461 | token: token, 462 | handler: handler 463 | }) 464 | return token 465 | }, 466 | 467 | // 广播频道 468 | publish: function(channel, data) { 469 | if(!_channels[channel]) return false 470 | // 获取频道订阅者 471 | var subscribers = _channels[channel] 472 | var len = subscribers.length 473 | // 后订阅先触发 474 | while(len--) { 475 | subscribers[len].handler(data, channel, subscribers[len].token) 476 | } 477 | return this 478 | }, 479 | 480 | // 移除订阅 481 | unsubscribe: function(token) { 482 | for(var channel in _channels) { 483 | var len = _channels[channel].length 484 | for(var index=0; index 通过外观模式对接口的二次封装隐藏其复杂性,可以简化用户的使用,外观模式也可以结合Module(模块)模式使用,需要注意的是外观模式会产生隐性成本,在设计时需要衡量是否需要使用外观模式抽象和封装某些结构。 756 | 757 | 758 | ## Factory(工厂)模式 759 | 760 | ### 简单工厂模式 761 | 762 | 简单工厂模式(静态工厂方法)主要用于创建同一类对象。该模式通过创建一个新对象然后包装增强其属性和功能实现,需要注意的是使用该模式它们的方法不能共享,不能像原型继承那样共享原型方法。 763 | 764 | ``` javascript 765 | function createPerson(type, name) { 766 | var person 767 | var person = new Object() 768 | person.name = name 769 | person.getName = function() { 770 | return this.name 771 | } 772 | return person 773 | } 774 | 775 | let person1 = createPerson('father', 'ziyi1') 776 | let person2 = createPerson('children', 'ziyi2') 777 | ``` 778 | 779 | 也可以增强简单工厂模式, 使其可以创建不同类的对象 780 | 781 | ``` javascript 782 | function createPerson(type, name) { 783 | 784 | var person 785 | 786 | switch(type) { 787 | case 'father': 788 | // 父亲差异部分 789 | // 例如 person = new Father(name) 790 | break 791 | case 'mother': 792 | // 母亲差异部分 793 | // 例如 person = new Mother(name) 794 | break 795 | case 'children': 796 | // 孩子差异部分 797 | // 例如 person = new Children(name) 798 | break 799 | default: 800 | break 801 | } 802 | return person 803 | } 804 | ``` 805 | 806 | ### 工厂方法模式 807 | 808 | 简单工厂模式创建多类对象相对不够灵活,工厂方法模式可以轻松创建多个类的实例对象 809 | 810 | ``` javascript 811 | function Person(type, name, age, job) { 812 | // 安全模式,外部可以不使用new关键字 813 | if(this instanceof Person) { 814 | return new this[type](name, age, job) 815 | } else { 816 | return new Person(type, name, age, job) 817 | } 818 | } 819 | 820 | Person.prototype = { 821 | constructor: Person, 822 | // Father类 823 | Father: function(name, age, job) { 824 | this.name = name 825 | this.age = age 826 | this.job = job 827 | console.log(`Father: ${name},${age},${job}`) 828 | }, 829 | 830 | // Mother类 831 | Mother: function(name, age, job) { 832 | this.name = name 833 | this.age = age 834 | this.job = job 835 | console.log(`Mother: ${name},${age},${job}`) 836 | }, 837 | 838 | // Children类 839 | Children: function(name, age, job) { 840 | this.name = name 841 | this.age = age 842 | this.job = job 843 | console.log(`Children: ${name},${age},${job}`) 844 | } 845 | } 846 | 847 | 848 | let data = [{ 849 | type: 'Father', 850 | name: 'ziyi2', 851 | age: 44, 852 | job: 'web' 853 | }, { 854 | type: 'Mother', 855 | name: 'ziyi2', 856 | age: 280, 857 | job: 'web' 858 | }, { 859 | type: 'Children', 860 | name: 'ziyi2', 861 | job: 'web' 862 | }] 863 | 864 | for(let person of data) { 865 | Person(person.type, person.name, person.age, person.job) 866 | } 867 | ``` 868 | 869 | > 在项目中通过对产品类的抽象使其创建业务主要负责用于创建多类产品的实例。 870 | 871 | ### 抽象工厂模式 872 | 873 | 874 | 抽象类是一种声明但是不能使用的类,JavaScript中有一个保留的关键字abstract对应抽象概念,抽象类在实例化时应该抛出错误。抽象类主要用于定义产品簇,并声明一些必备的方法,如果子类没有重写这些方法,那么子类实例化后调用这些方法时应该抛出错误。 875 | 876 | 抽象类中定义的方法只是显性的定义一些功能,但没有具体的实现,不能使用抽象类创建真实的使用实例对象。 877 | 878 | ``` javascript 879 | 880 | function objectCreate(o) { 881 | function F() {} 882 | F.prototype = o 883 | return new F() 884 | } 885 | 886 | /** 887 | * @Author: zhuxiankang 888 | * @Date: 2018-06-14 09:17:50 889 | * @Desc: 抽象工厂方法 890 | * @Parm: subClass -> 子类 891 | * abstractClass -> 抽象类 892 | */ 893 | function AbstractFactory (subClass, abstractClass) { 894 | if(typeof AbstractFactory[abstractClass] == 'function') { 895 | // 注意和寄生组合式继承中var prototype = objectCreate(AbstractFactory[abstractClass].prototype)的区别 896 | // 这里是不仅继承了抽象类的原型对象的方法 897 | // 还继承了抽象类的实例对象的方法和属性 898 | // 继承构造函数中没有继承抽象类的实例对象的方法和属性 899 | var prototype = objectCreate(new AbstractFactory[abstractClass]()) 900 | prototype.constructor = subClass 901 | subClass.prototype = prototype 902 | } else { 903 | throw new Error(abstractClass + ' undefined!') 904 | } 905 | } 906 | 907 | // Person抽象类 908 | AbstractFactory.Person = function() { 909 | // 实例属性会被子类继承 910 | this.type = 'person' 911 | } 912 | 913 | // Person抽象类声明的抽象方法(抽象方法不能被Person抽象类的实例使用) 914 | AbstractFactory.Person.prototype = { 915 | // constructor: AbstractFactory.Person, 916 | 917 | getType: function() { 918 | return new Error('abstract method "getType" can not be called!') 919 | }, 920 | 921 | getName: function() { 922 | return new Error('abstract method "getName" can not be called!') 923 | } 924 | } 925 | 926 | // 创建其他抽象类 927 | // AbstractFactory.?.prototype 928 | 929 | 930 | // 创建继承抽象类的子类 931 | function Father(name) { 932 | this.name = name 933 | } 934 | 935 | 936 | function Mother(name) { 937 | this.name = name 938 | } 939 | 940 | // 抽象工厂方法实现对抽象类的继承 941 | AbstractFactory(Father, 'Person') 942 | AbstractFactory(Mother, 'Person') 943 | 944 | 945 | // 需要注意放在AbstractFactory工厂方法之后 946 | // 因为工厂方法里重写了子类的prototype原型对象 947 | Mother.prototype.getType = function() { 948 | return this.type 949 | } 950 | 951 | 952 | var father = new Father('ziyi2') 953 | // person 954 | console.log(father.type) 955 | // Error: abstract method "getType" can not be called! 956 | console.log(father.getType()) 957 | 958 | var mother = new Mother('ziyi2') 959 | // person 根据原型链,子类的该方法重写了从父类继承的方法 960 | console.log(mother.getType()) 961 | ``` 962 | 963 | 964 | > 关于寄生组合式继承请查看[js类和继承](https://ziyi2.github.io/2018/06/05/js%E7%B1%BB%E5%92%8C%E7%BB%A7%E6%89%BF.html#more)。抽象工厂模式中的抽象类创建的不是一个真实的对象实例,而是一个类簇,抽象类指定了类的结构,区别于简单工厂模式创建单一对象,工厂方法模式创建多类对象。不过这种模式应用的并不广泛,因为JavaScript中不支持抽象化创建于虚拟方法。 965 | 966 | 967 | ## Mixin(混入)模式 968 | 969 | Mixin是可以轻松被一个子类或一组子类轻松继承功能的类,目的是函数复用。 970 | 971 | ### 继承Mixin 972 | 973 | 在JavaScript中,可以使用原型链轻松实现继承Mixin,具体可参考[寄生组合式继承](https://ziyi2.github.io/2018/06/05/js%E7%B1%BB%E5%92%8C%E7%BB%A7%E6%89%BF.html#more),Mixin类的方法和属性可以轻松被子类继承。 974 | 975 | ### Mixin(混入) 976 | 977 | Mixin允许对象通过较低的复杂性借用(继承)功能。继承Mixin可以实现父类的属性和方法被多个子类继承,但是在复杂的业务场景中可能存在一个子类需要继承多个父类的情况。 978 | 979 | Mixin对象可以被视为具有可以在很多其他对象原型中轻松共享属性和方法的对象。 980 | 981 | 982 | ``` javascript 983 | 984 | 985 | // mixin 混入对象 986 | // extend 被混入的对象 987 | var mixins = function(extend, mixin) { 988 | // 指定特定的混入属性 989 | if(arguments[2]) { 990 | for(var i=2,len=arguments.length; i Mixin有助于减少系统中的重复功能及增加函数复用,但是也存在一些缺陷,将Mixin导入对象原型会导致函数起源方面的不确定性以及原型污染。 1051 | 1052 | 1053 | ## 装饰者模式 1054 | 1055 | 装饰者模式旨在促进代码复用,提供了将行为添加至系统现有的类的功能,相对于类原有的基本功能来说不是必要的,如果是必要的,可以被合并到父类。装饰者在不改变原有对象的基本功能的基础上,对其功能进行扩展。 1056 | 1057 | 1058 | ```javascript 1059 | function MacBook() { 1060 | this.cost = function() { 1061 | return 997 1062 | } 1063 | this.screenSize = function() { 1064 | return 11.6 1065 | } 1066 | } 1067 | 1068 | 1069 | function Memory(macbook) { 1070 | var v = macbook.cost() 1071 | macbook.cost = function() { 1072 | return v + 75 1073 | } 1074 | } 1075 | 1076 | function Engraving(macbook) { 1077 | var v = macbook.cost() 1078 | macbook.cost = function() { 1079 | return v +200 1080 | } 1081 | } 1082 | 1083 | 1084 | var macbook = new MacBook() 1085 | Memory(macbook) 1086 | Engraving(macbook) 1087 | //1272 1088 | console.log(macbook.cost()) 1089 | ``` 1090 | 1091 | > 如果使用ES6语法,具体可以查看[ES6中的装饰者](http://es6.ruanyifeng.com/#docs/decorator)。装饰者模式如果管理不当,会极大的复杂化应用程序架构,因为向命名空间中引入了很多小型但类似的对象。 1092 | 1093 | 1094 | 1095 | ``` javascript 1096 | var decorator = function(dom, fn) { 1097 | if(typeof dom.onclick === 'function') { 1098 | // 获取原有的点击事件 1099 | var origin = dom.onclick 1100 | // 装饰dom的点击事件 1101 | dom.onclick = function(event) { 1102 | // 保留原有事件 1103 | origin.call(dom, event) 1104 | // 装饰新事件 1105 | fn.call(dom, event) 1106 | } 1107 | } else { 1108 | dom.onclick = fn() 1109 | } 1110 | } 1111 | 1112 | 1113 | var btn = document.getElementById('btn') 1114 | // 原有的点击事件 1115 | btn.onclick = function(event) { 1116 | console.log('origin btn click') 1117 | } 1118 | 1119 | function fn(event) { 1120 | console.log('decorator btn click') 1121 | } 1122 | 1123 | // 装饰btn 1124 | decorator(btn, fn) 1125 | ``` 1126 | 1127 | > 按钮原有的点击事件功能不变,在此基础上对按钮的点击功能进行了装饰。 1128 | 1129 | 1130 | 1131 | ## Flyweight(享元)模式 1132 | 1133 | 1134 | ``` javascript 1135 | // 享元接口 1136 | var Flyweight = { 1137 | serverName: function() {}, 1138 | getName: function() {} 1139 | } 1140 | 1141 | // 享元具体实现 1142 | function ConcreteFlyweight(newName) { 1143 | var _name = newName 1144 | 1145 | // 如果已经为某一功能定义接口,则实现该功能 1146 | if(typeof this.getName === "function") { 1147 | this.getName = function() { 1148 | return _name 1149 | } 1150 | } 1151 | 1152 | if(typeof this.serverName === "function") { 1153 | this.serverName = function(context) { 1154 | console.log('Server name ' + _name + ' to table number ' + context.getTable()) 1155 | } 1156 | } 1157 | } 1158 | 1159 | 1160 | // 实现接口方法 1161 | Function.prototype.implementsFor = function(Interface) { 1162 | if(Interface.constructor === Function) { 1163 | this.prototype = new Interface() 1164 | this.prototype.parent = Interface.prototype 1165 | } else { 1166 | this.prototype = Interface 1167 | this.prototype.parent = Interface 1168 | } 1169 | this.prototype.constructor = this 1170 | } 1171 | 1172 | // 实现接口 1173 | ConcreteFlyweight.implementsFor(Flyweight) 1174 | 1175 | 1176 | function TableContext(tableNumber) { 1177 | return { 1178 | getTable: function() { 1179 | return tableNumber 1180 | } 1181 | } 1182 | } 1183 | 1184 | 1185 | // 享元工厂 1186 | function FlyweightFactory() { 1187 | var _names = [] 1188 | 1189 | return { 1190 | getName: function(name) { 1191 | _name = new ConcreteFlyweight(name) 1192 | _names.push(_name) 1193 | return _name 1194 | }, 1195 | 1196 | getTotal: function() { 1197 | return _names.length 1198 | } 1199 | } 1200 | } 1201 | 1202 | 1203 | // 示例 1204 | var names = new ConcreteFlyweight() 1205 | var tables = new TableContext() 1206 | 1207 | var orders = 0 1208 | var flyweightFactory 1209 | function takeOrder(name, table) { 1210 | names[orders] = flyweightFactory.getName(name) 1211 | tables[orders ++] = new TableContext(table) 1212 | } 1213 | 1214 | flyweightFactory = new FlyweightFactory() 1215 | 1216 | takeOrder('ziyi2', 1) 1217 | takeOrder('ziyi2', 2) 1218 | 1219 | takeOrder('ply', 1) 1220 | takeOrder('ply', 4) 1221 | 1222 | for(var i=0; i 创建的对象更复杂,是一个复合对象。 1526 | 1527 | 1528 | 1529 | ## 适配器模式 1530 | 1531 | 将一个类(对象)的接口(方法或属性)转化成另外一个接口,从而满足用户的需求。 1532 | 1533 | 例如将之前的外观模式适配jQuery库 1534 | 1535 | ``` javascript 1536 | var Browser = { 1537 | event: { 1538 | add: function(dom, type, fn) { 1539 | $(dom).on(type, fn) 1540 | }, 1541 | 1542 | remove: function(dom, type, fn) { 1543 | $(dom).off(type, fn) 1544 | } 1545 | 1546 | // .... 1547 | }, 1548 | 1549 | id: function(dom, id) { 1550 | return $(id).get(0) 1551 | } 1552 | } 1553 | ``` 1554 | 1555 | > 需要注意适配的时候为了做到兼容参数此时不能变。 1556 | 1557 | 1558 | 除了代码适配,还可以进行参数适配。当一个函数传入参数较多时,很难记住参数的顺序,因此可以通过传入对象来进行参数适配 1559 | 1560 | ``` javascript 1561 | function fn(name, age, job, desc) {} 1562 | 1563 | function adapter(obj) { 1564 | var _adapter = { 1565 | name: '', 1566 | age: 0, 1567 | job: 'web', 1568 | desc: '' 1569 | } 1570 | 1571 | // 适配传入的参数 1572 | for(var key in _adapter) { 1573 | _adapter[key] = obj[key] || _adapter[key] 1574 | } 1575 | } 1576 | ``` 1577 | 1578 | 1579 | 1580 | ## MV* 1581 | 1582 | ### MVC 1583 | 1584 | 1585 | 1586 | MVC(模型-视图-控制器)是一种架构设计模式,通过关注点分离鼓励改进引用程序组织。它强制将业务数据(Model)、用户界面(View)隔离,并将控制器(Controller)用于管理逻辑和用户输入。在JavaScript中的MVC框架包括Backbone、Ember.js和AngularJS。 1587 | 1588 | ![MVC](https://raw.githubusercontent.com/ziyi2/js/master/images/MVC.png) 1589 | 1590 | 1591 | MVC有利于简化应用程序功能的模块化。 1592 | - 整体维护更容易。 1593 | - 解耦Model和View。 1594 | - 消除Model和Controlle的代码重复。 1595 | 1596 | 1597 | #### Model 1598 | 1599 | 代表特定的数据,当Model改变时,会通过观察者模式发布信息。一般要求数据可持久化,保存在内存中、用户的localStorage数据存储中或者数据库中。Model主要和业务数据相关。 1600 | 1601 | #### View 1602 | 描绘Model当前状态,会通过观察者模式订阅Model更新或修改的信息,从而做出View的更新。一个View通常订阅一个Model,用户与View进行交互,包括读取和编辑Model。 1603 | 1604 | #### Controller 1605 | 处理用户交互,为View做决定,是Model和View的中介。当用户操作View时,通常用于更新Model。 1606 | 1607 | 1608 | #### 优点 1609 | 1610 | - MVC有利于简化应用程序功能的模块化。 1611 | - 解耦Model和View。 1612 | - 消除Model和Controlle的代码重复。 1613 | 1614 | #### 缺点 1615 | - Controller测试困难。因为视图同步操作是由View自己执行,而View只能在有UI的环境下运行。在没有UI环境下对Controller进行单元测试的时候,Controller业务逻辑的正确性是无法验证的:Controller更新Model的时候,无法对View的更新操作进行断言。 1616 | - View无法组件化。View是强依赖特定的Model的,如果需要把这个View抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的Domain Model是不一样的 1617 | 1618 | 1619 | 1620 | ### MVC Model 2 1621 | 1622 | 在Web服务端开发的时候也会接触到MVC模式,而这种MVC模式不能严格称为MVC模式。经典的MVC模式只是解决客户端图形界面应用程序的问题,而对服务端无效。服务端的MVC模式又自己特定的名字:MVC Model 2,或者叫JSP Model 2,或者直接就是Model 2 。Model 2客户端服务端的交互模式如下: 1623 | 1624 | ![MVC Model2](https://raw.githubusercontent.com/ziyi2/js/master/images/MVC%20Model2.png) 1625 | 1626 | 1627 | 服务端接收到来自客户端的请求,服务端通过路由规则把这个请求交由给特定的Controller进行处理,Controller执行相应的业务逻辑,对数据库数据(Model)进行操作,然后用数据去渲染特定的模版,返回给客户端。 1628 | 1629 | 因为HTTP协议是单工协议并且是无状态的,服务器无法直接给客户端推送数据。除非客户端再次发起请求,否则服务器端的Model的变更就无法告知客户端。所以可以看到经典的Smalltalk-80 MVC中Model通过观察者模式告知View更新这一环被无情地打破,不能称为严格的MVC。 1630 | 1631 | Model 2模式最早在1998年应用在JSP应用程序当中,JSP Model 1应用管理的混乱诱发了JSP参考了客户端MVC模式,催生了Model 2。 1632 | 1633 | 1634 | ![MVC Model](https://raw.githubusercontent.com/ziyi2/js/master/images/MVC%20Model.png) 1635 | 1636 | 后来这种模式几乎被应用在所有语言的Web开发框架当中。PHP的ThinkPHP,Python的Dijango、Flask,NodeJS的Express,Ruby的RoR,基本都采纳了这种模式。平常所讲的MVC基本是这种服务端的MVC。 1637 | 1638 | 1639 | ### MVP 1640 | 1641 | MVP(模型-视图-表示器)是MVC设计模式的一种衍生模式,专注于改进表示逻辑。MVP打破了View原来对于Model的依赖,其余的依赖关系和MVC模式一致。 1642 | 1643 | 1644 | ![MVP](https://raw.githubusercontent.com/ziyi2/js/master/images/MVP.png) 1645 | 1646 | 1647 | 和MVC模式一样,用户对View的操作都会从View交移给Presenter。Presenter同样的会执行相应的业务逻辑,并且对Model进行相应的操作;而这时候Model也是通过观察者模式把自己变更的消息传递出去,但是传给Presenter而不是View。Presenter获取到Model变更的消息以后,通过View提供的接口更新界面。 1648 | 1649 | 1650 | 对比在MVC中,Controller是不能操作View的,View也没有提供相应的接口;而在MVP当中,Presenter可以操作View,View需要提供一组对界面操作的接口给Presenter进行调用;Model仍然通过事件广播自己的变更,但由Presenter监听而不是View。 1651 | 1652 | 1653 | 1654 | #### 优点 1655 | 1656 | - 便于测试。Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候。可以通过Mock一个View对象,这个对象只需要实现了View的接口即可。然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter业务逻辑的正确性。这里根据上面的例子给出了Presenter的单元测试样例。 1657 | 1658 | - View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务逻辑完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做高度可复用的View组件。 1659 | 1660 | 1661 | #### 缺点 1662 | 1663 | - Presenter中除了业务逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。 1664 | 1665 | ### MVVM 1666 | 1667 | MVVM可以看作是一种特殊的MVP(Passive View)模式,或者说是对MVP模式的一种改良。 1668 | 1669 | MVVM代表的是Model-View-ViewModel,这里需要解释一下什么是ViewModel。ViewModel的含义就是 "Model of View",视图的模型。它的含义包含了领域模型(Domain Model)和视图的状态(State)。 在图形界面应用程序当中,界面所提供的信息可能不仅仅包含应用程序的领域模型。还可能包含一些领域模型不包含的视图状态,例如电子表格程序上需要显示当前排序的状态是顺序的还是逆序的,而这是Domain Model所不包含的,但也是需要显示的信息。 1670 | 1671 | 可以简单把ViewModel理解为页面上所显示内容的数据抽象,和Domain Model不一样,ViewModel更适合用来描述View。 1672 | 1673 | 1674 | ![MVVM](https://raw.githubusercontent.com/ziyi2/js/master/images/MVVM.png) 1675 | 1676 | 1677 | MVVM的调用关系和MVP一样。但是,在ViewModel当中会有一个叫Binder,或者是Data-binding engine的东西。以前全部由Presenter负责的View和Model之间数据同步操作交由给Binder处理。你只需要在View的模版语法当中,指令式地声明View上的显示的内容是和Model的哪一块数据绑定的。当ViewModel对进行Model更新的时候,Binder会自动把数据更新到View上去,当用户对View进行操作(例如表单输入),Binder也会自动把数据更新到Model上去。这种方式称为:Two-way data-binding,双向数据绑定。可以简单而不恰当地理解为一个模版引擎,但是会根据数据变更实时渲染。 1678 | 1679 | 1680 | 也就是说,MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交由框架所提供的Binder进行负责。只需要告诉Binder,View显示的数据对应的是Model哪一部分即可。 1681 | 1682 | 1683 | #### 优点 1684 | 1685 | - 提高可维护性。解决了MVP大量的手动View和Model同步的问题,提供双向绑定机制。提高了代码的可维护性。 1686 | - 简化测试。因为同步逻辑是交由Binder做的,View跟着Model同时变更,所以只需要保证Model的正确性,View就正确。大大减少了对View同步更新的测试。 1687 | 1688 | 1689 | #### 缺点 1690 | 1691 | - 过于简单的图形界面不适用,或说牛刀杀鸡。 1692 | - 对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高。 1693 | - 数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的。 1694 | -------------------------------------------------------------------------------- /JavaScript权威指南.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/JavaScript权威指南.docx -------------------------------------------------------------------------------- /JavaScript高级程序设计.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/JavaScript高级程序设计.docx -------------------------------------------------------------------------------- /images/Flyweight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/images/Flyweight.png -------------------------------------------------------------------------------- /images/MVC Model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/images/MVC Model.png -------------------------------------------------------------------------------- /images/MVC Model2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/images/MVC Model2.png -------------------------------------------------------------------------------- /images/MVC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/images/MVC.png -------------------------------------------------------------------------------- /images/MVP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/images/MVP.png -------------------------------------------------------------------------------- /images/MVVM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/images/MVVM.png -------------------------------------------------------------------------------- /images/Observer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/images/Observer.png -------------------------------------------------------------------------------- /images/PublishSubscribe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/images/PublishSubscribe.png -------------------------------------------------------------------------------- /images/pubsub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/images/pubsub.png -------------------------------------------------------------------------------- /js.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/js.html -------------------------------------------------------------------------------- /visio/Flyweight.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/visio/Flyweight.vsdx -------------------------------------------------------------------------------- /visio/MVC.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/visio/MVC.vsdx -------------------------------------------------------------------------------- /visio/MVP.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/visio/MVP.vsdx -------------------------------------------------------------------------------- /visio/MVVM.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/visio/MVVM.vsdx -------------------------------------------------------------------------------- /visio/Observer.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/visio/Observer.vsdx -------------------------------------------------------------------------------- /visio/PublishSubscribe.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/visio/PublishSubscribe.vsdx -------------------------------------------------------------------------------- /visio/pubsub.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acexyf/js/0e9992f3eecc199a79a0b22cc17723729714f9ff/visio/pubsub.vsdx -------------------------------------------------------------------------------- /高性能JS.md: -------------------------------------------------------------------------------- 1 | # 如何使JS提高运行性能 2 | 3 | ## 数据的存取 4 | 5 | Js中的存储方案 6 | 7 | - 字面量:字符串、数字、布尔值、对象、数组、函数、正则表达式、null、undefined 8 | - 本地变量:var定义的数据存储单元 9 | - 数组元素:存储在js数组对象内部,以数字作为索引 10 | - 对象成员:存储在js对象内部,以字符串作为索引 11 | 12 | > 从字面量和局部变量中存取数据性能差异微不足道,访问数组和对象成员性能代价高一些。如果在乎运行速度,尽量使用字面量和局部变量,减少数组项和对象成员的使用。 13 | 14 | 15 | ### 作用域链 16 | 17 | 通过原型链知道函数都是Function对象的实例,Function对象和其他对象一样,拥有可以编程访问的属性,其中一个内部属性[[Scope]]包含了函数被创建的作用域中的对象的集合,这个集合被称为作用域链,作用域链决定了哪些数据可以被函数访问。作用域中的每个对象被称为可变对象,每个可变对象都以键值对的形式存在。 18 | 19 | ``` html 20 | 26 | ``` 27 | 28 | 当add函数被初始化时(注意不是在add被执行的时候),在作用域链的顶部插入一个全局对象,代表着全局范围内定义的变量的集合,这些变量在add函数内可以被访问。 29 | 30 | | add函数 | 作用域链 | 全局对象 | 31 | | :-------- | :--------| :------ | 32 | | [[Scope]] | 0 | this/window/document/add(注意此时add本身也是全局变量) | 33 | 34 | > add函数的[[Scope]]属性指向了作用域链,作用域链中的首个对象指向全局对象(全局变量的集合)。 35 | 36 | 当add函数被执行时,会创建一个执行环境(执行上下文)的内部对象,一个执行环境定义了一个函数执行时的环境(函数每次的执行环境都是独一无二的),多次调用同一个函数会导致创建多个执行环境,函数执行完毕执行环境会被销毁。全局执行环境直到关闭网页或浏览器才会被销毁。 37 | 38 | 39 | 40 | 每个执行环境都有自己对应的作用域链,用于解析标识符。执行环境被创建的时候,作用域链初始化为当前运行函数的[[Scope]]属性中的对象。这些对象按照出现在函数中的顺序被复制到执行环境的作用域链中(前面说的全局对象,是在初始化的时候就在[[Scope]]中创建完成,而这里所说的是在执行函数中的除全局对象以外的和执行函数相关的局部变量、命名参数、参数集合以及this),作用域链中复制的出现在函数中的对象(局部变量、命名参数、参数集合以及this)的新对象被称为活动对象。活动对象会被推入作用域链的最前端(后面是当前的执行环境的父执行环境的活动对象...一直到最后是全局对象) 41 | 42 | | add函数 | 作用域链 | 活动对象和全部对象 | 43 | | :-------- | :--------| :------ | 44 | | [[Scope]] | 0 | this/arguments/num1/num2/sum | 45 | | [[Scope]] | 1 | this/window/document/add(注意此时add本身也是全局变量) | 46 | 47 | 函数在执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或存储数据。该过程主要是依靠作用域链进行解析,首先搜索当前执行环境的活动对象,如果找到标识符对应的变量则终止搜索,否则继续搜索作用域链的下一个对象,搜索的过程会持续进行,直到全局对象,如果在全局对象中都没有匹配标识符的变量,那么该变量是未定义的(undefined)。 48 | 49 | > 搜索是会消耗性能的,可以清楚的知道当前执行环境的活动对象的搜索是最快的,而全局对象的搜索则是最耗性能的。并且可以发现如果存在相同的变量,根据作用域链的搜索顺序决定先搜索到的变量被执行。如果某个跨作用域链的值在当前执行环境中被引用不止一次时,可以存储到局部变量中从而提升搜索性能。 50 | 51 | 52 | 需要注意作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。 53 | 54 | 55 | ### 改变作用域链 56 | 57 | #### with 58 | 59 | 尽量不要使用with,可能会使局部变量的访问代价更大,因为with会创建新的作用域。 60 | 61 | 62 | #### catch 63 | 64 | 65 | try代码如果发生错误,执行过程会自动跳转到catch子句,然后把异常对象推入一个变量对象并置于作用域的首位,在catch代码块内部,函数所有的局部变量将会放在第二个作用域链对象中。 66 | 67 | 为了尽量简化代码使得catch子句对性能的影响最小化,推荐的做法是将错误委托给一个函数来处理 68 | 69 | ``` html 70 | 77 | ``` 78 | 79 | > 函数fn2是catch子句中唯一执行的代码,该函数接受错误产生的异常对象为参数,由于只执行一条语句,且没有局部变量的访问,作用域的临时改变不会影响代码性能。需要注意with或catch或eval子句被称为动态作用域,动态作用域只存在代码执行过程,无法通过静态分析,因此只有确实必要时才推荐使用动态作用域。 80 | 81 | ### 闭包、作用域和内存 82 | 83 | 闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数。 84 | 85 | 闭包可以访问局部作用域之外的数据,可能会导致性能问题。通常函数在执行完毕后会销毁执行环境,内存中仅保存全局作用域Global variable object,但是闭包的情况不同 86 | 87 | ``` html 88 | 99 | ``` 100 | 101 | | fn执行环境 | 作用域链 | 活动对象和全部对象 | 102 | | :-------- | :--------| :------ | 103 | | [[Scope]] | 0 | this/arguments/id | 104 | | [[Scope]] | 1 | this/window/fn | 105 | 106 | 107 | 108 | | 匿名函数(闭包) | 作用域链 | 活动对象和全部对象 | 109 | | :-------- | :--------| :------ | 110 | | [[Scope]] | 0 | this/arguments/id | 111 | | [[Scope]] | 1 | this/window/fn | 112 | 113 | 当fn被执行的时候,fn执行环境对应的活动对象被创建(包含id变量),成为作用域链中的最前面的对象,需要注意此时匿名函数被创建(fn执行的过程中创建了匿名函数),匿名函数的[[Scope]]属性被初始化为fn执行环境对应的活动对象和全局对象(书上说是fn的作用域链,这里表述并不清楚,因为fn的作用域链是会被销毁的,但是fn的活动对象不会)。如果此时匿名函数被其他变量进行引用,那么尽管fn执行完毕,但是fn执行环境对应的活动对象并不会被销毁(执行环境中的作用域链仍然会被销毁,活动对象仍然存在内存中),因为很简单啊,匿名函数执行的时候还需要访问fn执行环境的活动对象啊(意味着使用闭包需要更多的内存开销)。 114 | 115 | 当被引用的匿名函数执行时,会创建新的执行环境,它的作用域链与属性[[Scope]]中所引用的两个对象一起被初始化,新的执行环境会创建新的活动对象并把它插入作用域链的首位 116 | 117 | 118 | | 匿名函数(闭包) | 作用域链 | 对象 | 类型 | 119 | | :-------- | :--------| :------ | :------ | 120 | | [[Scope]] | 0 | this/obj1/obj2/value1/value2/arguments | 闭包活动对象 | 121 | | [[Scope]] | 1 | this/arguments/id | fn活动对象 | 122 | | [[Scope]] | 2 | this/window/fn | 全局对象 | 123 | 124 | 需要注意的是闭包产生了新的作用域,fn活动对象被排在了闭包活动对象之后,此时访问fn活动对象不可避免的会带来性能损失(此时如果fn活动对象以及全局对象访问比较频繁,则仍然可以开辟局部变量进行缓存,提升执行速度)。 125 | 126 | ``` html 127 | 135 | ``` 136 | 137 | ### 对象成员 138 | 139 | - 原型 140 | - 原型链 141 | 142 | 搜索方法和属性的时候和作用域链类似,先搜索实例对象的属性和方法,如果未找到,则继续搜索原型对象的属性和方法。因此搜索原型对象的属性和方法会带来一定的性能损耗(属性或方法在原型链中存在的位置越深,则消耗的性能越大)。需要注意的是读取实例对象的数据会比读取对象字面量或局部变量的数据更消耗性能。 143 | 144 | > 此时仍然可以通过缓存,将对象成员赋值给局部变量从而提升读写性能。 145 | 146 | ### 总结 147 | 148 | 如何提升存取性能 149 | 150 | - 1. 访问字面量和局部变量的速度最快,访问数组元素和对象成员相对较慢。 151 | - 2. 由于局部变量存在于作用域链的起始位置,因此访问局部变量比访问跨作用域变量更快,全局变量的访问速度最慢。 152 | - 3. 避免使用with和catch,除非是有必要的情况下。 153 | - 4. 嵌套的对象成员会明显影响性能,尽量少用,例如window.loacation.href。 154 | - 5. 属性和方法在原型链中的位置越深,则访问它的速度也越慢。 155 | - 6. 通常来说,需要访问多次的对象成员、数组元素、跨作用域变量可以保存在局部变量中从而提升js执行效率。 156 | 157 | 158 | ## 算法和流程控制 159 | 160 | 代码数量少并不意味着运行速度就快。代码数量多也不以为这运行速度一定慢。 161 | 162 | ### 循环 163 | 164 | 代码的执行时间大部分消耗在循环中。 165 | 166 | #### 循环类型 167 | 168 | - for 169 | - while 170 | - do...while 171 | - for...in...(性能最差) 172 | - for...of...(可以想象性能就算优化过也会比普通循环速度慢) 173 | 174 | > for...in...速度慢的原因是不仅遍历实例的属性还会遍历原型链中继承而来的属性。 175 | 176 | #### 循环性能 177 | 178 | 179 | ##### 性能测试 180 | 181 | ###### 循环次数较少 182 | 183 | ``` html 184 | 245 | 246 | ``` 247 | 248 | > 数量极少的时候for速度最快,for...in...和for...of...性能差不多,在循环该测试是在chrome浏览器下。 249 | 250 | ###### 循环次数较多 251 | 252 | 253 | ``` html 254 | 290 | 291 | ``` 292 | 293 | > 当循环次数10000甚至是100000以上时,这里以100000次为例,发现for循环性能明显高于for...of,而for...of的性能则又高于for...in...。但是在循环10000次以下的时候,又发现for...of的性能略低于for...in...,但是for...of...的性能是有极大优化空间的,所以for...of和for...in还是推荐使用for...of...。 294 | 295 | 296 | ##### 使用for循环仍然可以优化 297 | 298 | 299 | ``` html 300 | 322 | 323 | ``` 324 | 325 | > 但是到百万级别次数甚至以上的时候发现居然还是++耗时少,因此这里觉得没有必要对++ --过多执着。 326 | 327 | 328 | ##### 减少迭代次数 329 | 330 | 循环的性能主要和以下两者有关 331 | 332 | - 每次迭代处理的事务 333 | - 迭代的次数 334 | 335 | 事务可能很难被减少,但是迭代的次数是可以通过代码减少的,一种有效的办法就是使用“Duff装置”(Duff's Device),Duff装置是一种循环体展开技术,它使得一次迭代实际上执行了多次迭代的操作 336 | 337 | ``` html 338 | 367 | 368 | ``` 369 | 370 | 371 | ``` html 372 | 444 | ``` 445 | 446 | > 发现使用Duff装置后确实能提升性能。 447 | 448 | 449 | ##### 基于函数的迭代 450 | 451 | 需要注意类似于forEach等方法,尽管基于函数的迭代提供了更为便利的迭代方法,但仍然比基于循环的迭代要慢,因为每个数组项调用外部方法打来的开销是速度慢的主要原因。 452 | 453 | 454 | ``` html 455 | 516 | 517 | ``` 518 | 519 | ### 条件语句 520 | 521 | 在条件比较多的情况下switch语句比if-else性能高。 522 | 523 | > 大多数语言对switch语句的实现采用branch table(分支表)索引来进行优化,同时在js中switch语句比较值采用全等操作符,不会发生类型转换的性能损耗。当判断多余两个离散值时使用switch是更佳选择。 524 | 525 | #### 优化if-else 526 | 527 | 将最可能出现的条件放在最前面,从而减少条件判断次数 528 | 529 | 530 | ### 查找表 531 | 532 | if-else和switch比使用查找表慢很多,javascript中可以使用数组和普通对象来构建查找表,特别是在条件语句数量很大的时候。 533 | 534 | ``` html 535 | 559 | ``` 560 | 561 | > 整个过程变成数组查询或者对象成员查询,不用书写任何判断语句。 562 | 563 | ### 递归 564 | 565 | 递归可能会导致栈溢出,各个浏览器的调用栈大小不同,最常见的栈溢出原因是不正确的终止条件。为了能在浏览器中安全地工作应尽量将递归替换成迭代或者Memoization(Tabulation,主要用于加速程序计算的优化技术,使得函数避免重复演算之前被处理过的参数,而返回已缓存的结果。) 566 | 567 | - 使用迭代替换递归 568 | - 使用memoization优化递归性能 569 | 570 | 571 | ## 定时器优化性能 572 | 573 | UI线程:大多数浏览器让一个单线程共用于执行JavaScript和更新用户界面。任何时候只能执行一种操作,如果JavaScript代码正在执行,那么用户界面可能无法响应输入,或者UI界面正在更新,那么JavaScript代码无法执行。 574 | 575 | 576 | ### 浏览器UI线程 577 | 578 | 用于执行JavaScript和更新用户界面的进程通常被称为“浏览器UI线程”。 UI线程的工作基于一个简单的队列系统,任务会被保存在队列中直到进程空闲(空闲了以后会把下一个任务提取出来运行,任务包括执行JavaScript代码、执行UI更新[包括重绘和重排])。 579 | 580 | #### 运行多久合理 581 | 582 | 单个JavaScript操作花费的总时间最大值不应该超过100ms,超过100ms用户会觉得自己与界面失去联系。 583 | 584 | #### 使用定时器让出时间片段 585 | 586 | 如果有些任务确实需要超过100ms才能执行完毕,那么应该使用时间切片分段完成执行任务,从而在时间切片的空闲中可以使得UI有时间段进行更新(执行JavaScript的同时间断性的让出UI线程的控制权用来更新用户界面)。 587 | 588 | > 执行JavaScript(固定的时间段) -> 更新用户界面 -> 执行JavaScript(固定的时间段)-> 更新用户界面 ... 589 | 590 | 为了在执行耗时极大的JavaScript代码时可以让出UI线程的控制权,可以使用定时器。使用setTimeout或setInterval定时器时,会告诉JavaScript引擎先等待一段时间(这段时间可以进行其他队列任务的操作,包括UI界面更新等),然后添加一个JavaScript任务到UI线程队列。需要注意使用定时器的定时时间只是告知何时被加入UI线程的任务队列,并不是何时去执行这段代码,因此真正执行定时器任务的时间一定是大于等于定时时间。 591 | 592 | 593 | ``` html 594 | 595 | 617 | ``` 618 | 619 | > 真正执行的时间一定是大于定时时间的。 620 | 621 | 622 | 623 | ``` html 624 | 625 | 653 | ``` 654 | 655 | > 此时基本上定时器失去了定时能力,因为三个定时任务被加入队列任务后,都必须等待JavaScript的map循环执行完毕后才能执行,由于定时时间已经超时,因此三个定时任务一等到UI线程空闲就立马一个个排队执行,需要注意的是加入队列的执行顺序仍然按照定时时间的长短来决定。 656 | 657 | 658 | ``` html 659 | 677 | 678 | ``` 679 | 680 | > setInterval函数会重复添加JavaScript任务到UI队列,需要注意不同的地方是如果UI队列中已经存在同一个setInterval函数创建的任务,那么后续相同的任务不会被添加到UI队列中(一定是当前setInterval函数创建的任务执行完毕后才会新添加任务的UI队列)。 681 | 682 | 683 | ### 使用定时器处理数组 684 | 685 | - 处理过程不需要同步 686 | - 数据不需要按顺序处理 687 | 688 | 如果满足以上两者条件,可以使用定时器分解任务(防止大数据量数组计算导致UI队列无法更新视图产生停滞现象)。 689 | 690 | > 需要注意UI更新任务的时间如果小于25ms会不够用,因此定时的时长至少应大于25ms。 691 | 692 | 之前所说的JavaScript代码的执行时长不应该超过100ms,超过100ms用户会觉得自己与界面失去联系。如果需要处理大数据量数组,可以使用定时器保持50ms左右的运行时间,保证不会影响用户体验(50ms的时间足够UI更新视图)。 693 | 694 | 可以通过记录运行时间来处理 695 | 696 | 697 | ``` html 698 | 743 | ``` 744 | 745 | > 需要注意这种使用方式是页面上只有一个定时器的时候比较适用,如果有多个定时器容易出现性能问题,因为UI线程只有一个。 746 | 747 | ### Web Workers 748 | 749 | 通过Web Workers API可以使代码运行在UI线程之外。JavaScript和UI共享同一个进程,所以可以互相访问,但是Web Workers API由于创建独立的线程,因此和UI线程之间需要通过通信才能传递信息,UI线程中的大数据量处理可以通知Web Workers API进行处理,处理完毕后又可以返回处理结果告知UI线程进行处理,需要注意的是Web Workers API的兼容性很差,只有在IE11中兼容。 750 | 751 | 752 | ## 编程优化 753 | 754 | ### 避免双重求值 755 | 756 | 避免使用代码字符串执行脚本语言,例如使用eval()、Function构造函数、setTimeout()和setInterval()。 757 | 758 | ``` html 759 | 766 | ``` 767 | 768 | > 双重求值是一项代价昂贵的操作,比直接包含代码执行速度慢许多。 769 | 770 | 771 | ### 使用位操作 772 | 773 | 在位操作中,数字被转换为有符号32位格式,每次运算符会直接操作该32位数以得到结果,尽管需要转换,但是比布尔操作相比要快得多。 774 | 775 | 776 | ``` html 777 | 784 | ``` 785 | 786 | 787 | ``` html 788 | 807 | ``` 808 | 809 | #### 使用位掩码 810 | 811 | 812 | ``` html 813 | 838 | ``` 839 | 840 | > 左移就是乘以2,右移就是除以2。 841 | 842 | 843 | --------------------------------------------------------------------------------