原型 每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。
prototype 每个函数都有一个 prototype 属性,这个属性指向函数的原型对象,这个对象正是调用该构造函数而创建的实例 的原型
通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性 __proto__ 每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__
,这个属性会指向该对象的原型。
Object.prototype 这个对象是个例外,它的__proto__
值为null constructor 每个原型都有一个 constructor 属性指向关联的构造函数
1 2 3 4 5 6 7 8 9 10 11 function Person ( ) {} var person = new Person();console .log(person.__proto__ == Person.prototype) console .log(Person.prototype.constructor == Person) console .log(Object .getPrototypeOf(person) === Person.prototype) person.constructor === Person.prototype.constructor === Person
原型链 每个对象都有一个内部属性[[prototype]],通常称之为原型.原型的值可以是一个对象,也可以是null.如果它的值是一个对象,则这个对象也一定有自己的原型.这样就形成了一条线性的链,称之为原型链.
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
1 2 3 4 5 6 7 8 function getProperty (obj, prop ) { if (obj.hasOwnProperty(prop) return obj[prop]; else if (obj.__proto__ !== null ) return getProperty(obj.__proto__, prop); else return undefined ; } # obj.hasOwnProperty 是 JavaScript 中处理属性并且不会遍历原型链的方法之一。 # 另一种方法: Object.keys()
ES5的构造函数生成实例对象的传统方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function Point (x, y ) { if (new .target === Point) { this .name = name; } else { throw new Error ('必须使用 new 命令生成实例' ); } this .x = x; this .y = y; this .selfFunc = function ( ) {...}; } Point.prototype.toString = function ( ) { return '(' + this .x + ', ' + this .y + ')' ; }; var p = new Point(1 , 2 );
new的过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 1.以构造器的prototype为原型,创建新对象,从而可以访问原型属性和原型方法 let child = Object.create(Parent.prototype); Object.create = function (parentObj) { function F() {}; F.prototype = parentObj; return new F(); } 或者 let child = new Object(); child.__proto__ = Parent.prototype; child.constructor = Parent.prototype.constructor; 2.将this和调用参数传给构造器执行,从而设置实例属性和实例方法 let result = Parent.apply(child, rest); 3.如果构造器没有手动返回对象,则返回第一步的对象 return typeof result === 'object' ? result : child;
设置实例属性的一种方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Object .defineProperty( newObject, "someKey" , { value: "for more control of the property's behavior" , writable: true , enumerable: true , configurable: true }); Object .defineProperties( newObject, { "someKey" : { value: "Hello World" , writable: true }, "anotherKey" : { value: "Foo bar" , writable: false } });
继承 1. 原型链继承 问题:
继承的引用类型的属性被所有实例共享 在创建 Child 的实例时,不能向Parent传参 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Parent ( ) { this .type = "parent" ; this .names = ['kevin' , 'daisy' ]; } Parent.prototype.getNames = function ( ) { console .log(this .names); } function Child ( ) {} Child.prototype = new Parent(); Child.prototype.constructor = Child; var child1 = new Child();console .log(child1.getNames()); child1.names.push("Clark" ); child1.type = "child1" ; console .log(child1.type) var child2 = new Child();console .log(child2.getNames()); console .log(child2.type);
2. 借用构造函数(经典继承) 优点:
避免了引用类型的属性被所有实例共享 可以在 Child 中向 Parent 传参 缺点:
方法都在构造函数中定义,每次创建实例都会创建一遍方法 此方法只能继承Parent实例的属性和方法,无法继承Parent原型的属性和方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Parent ( ) { this .names = ['kevin' , 'daisy' ]; } Parent.prototype.getNames = function ( ) { console .log(this .names); } function Child ( ) { Parent.call(this ); } var child1 = new Child();child1.names.push('yayu' ); console .log(child1.names); var child2 = new Child();console .log(child2.names); console .log(child2.getNames());
3. 组合继承 - 组合原型链继承和经典继承 优点:
融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。 缺点:
会调用两次父构造函数 Child.prototype 上面会创建多余的属性 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 function Parent (name ) { this .name = name; this .colors = ['red' , 'blue' , 'green' ]; } Parent.prototype.getName = function ( ) { console .log(this .name) } function Child (name, age ) { Parent.call(this , name); this .age = age; } Child.prototype = new Parent(); Child.prototype.constructor = Child; var child1 = new Child('kevin' , '18' );child1.colors.push('black' ); console .log(child1.name); console .log(child1.age); console .log(child1.colors); var child2 = new Child('daisy' , '20' );console .log(child2.name); console .log(child2.age); console .log(child2.colors);
4. 原型式继承 - 非构造函数继承 就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
缺点:
包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。 1 2 3 4 5 function createObj (o ) { function F ( ) {} F.prototype = o; return new F(); }
5. 寄生式继承 创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
缺点:
跟借用构造函数模式一样,每次创建对象都会创建一遍方法。 1 2 3 4 5 6 7 function createObj (o ) { var clone = Object .create(o); clone.sayName = function ( ) { console .log('hi' ); } return clone; }
6. 寄生组合式继承 这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function Parent (name ) { this .name = name; this .colors = ['red' , 'blue' , 'green' ]; } Parent.prototype.getName = function ( ) { console .log(this .name) } function Child (name, age ) { Parent.call(this , name); this .age = age; } var F = function ( ) {};F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; var child1 = new Child('kevin' , '18' );console .log(child1);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function object (o ) { function F ( ) {} F.prototype = o; return new F(); } function prototype (child, parent ) { var prototype = object(parent.prototype); prototype.constructor = child; child.prototype = prototype; } prototype(Child, Parent);
7. 直接只继承prototype - 有缺陷 只继承父构造函数原型的属性和方法
1 2 3 Child.prototype = Parent.prototype; Child.prototype.constructor = Child;
8. 利用空对象作为中介,只继承prototype 只继承父构造函数原型的属性和方法
1 2 3 4 5 6 7 function extend (Child, Parent ) { var F = function ( ) {}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; Child.uber = Parent.prototype; }
9. 拷贝继承 只继承父构造函数原型的属性和方法
1 2 3 4 5 6 7 8 function copyExtend (Child, Parent ) { var p = Parent.prototype; var c = Child.prototype; for (var i in p) { c[i] = p[i]; } c.uber = p; }
10. 浅拷贝 - 非构造函数继承 存在父对象被篡改的可能
1 2 3 4 5 6 7 8 function extendCopy (p ) { var c = {}; for (var i in p) { c[i] = p[i]; } c.uber = p; return c; }
11. 深拷贝 - 非构造函数继承 1 2 3 4 5 6 7 8 9 10 11 12 function deepCopy (p, c ) { var c = c || {}; for (var i in p) { if (typeof p[i] === 'object' ) { c[i] = (p[i].constructor === Array ) ? [] : {}; deepCopy(p[i], c[i]); } else { c[i] = p[i]; } } return c; }
ES6引入class来定义类 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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 let methodName = "someFunc"; // 与函数一样,类也可以使用表达式的形式定义。 // const GlobalPointName = class Point { // 类的表达式中,Point只能类内部使用 class Point { // 实例属性 x=0; y=0; constructor(x, y) { // this 关键字代表实例对象 this.x = x || this.x; this.y = y || this.y; console.log(new.target === Point) // true if (new.target === Point) { throw new Error('本类不能实例化'); } } // 原型方法 toString() { return '(' + this.x + ', ' + this.y + ')'; } // 取值函数(getter)和存值函数(setter) get prop() { return 'getter'; } set prop(value) { console.log('setter: '+value); } // 类的属性名,可以采用表达式 [methodName]() {...} // 静态方法 // 注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。 // 静态方法可以与非静态方法重名。 // 父类的静态方法,可以被子类继承。 // 静态方法也是可以从super对象上调用的。 static staticFunc() {...} // 静态属性提案 static staticProp = 1; // 私有属性提案 // 私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性。 #count = 0; // 私有方法提案 #privateFunc() {} } // 静态属性 Point.staticProp = 1; // 类的继承 class Line extends Point { constructor(x, y) { // 必须调用super来实例化子类自己的this // 父类中的new.target会返回Line super(x, y); } static staticFunc() { return super.staticFunc() + ', too'; } } let p = new Point(1, 2); p.prop = 123; // setter: 123 p.prop // 'getter' Point.staticFunc(); // ES6中的class是构造函数的另一种写法 typeof Point // "function" Point === Point.prototype.constructor // true p.constructor === Point.prototype.constructor // true // 事实上,类的所有方法都定义在类的prototype属性上面。 // 另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable) // 在类的实例上面调用方法,其实就是调用原型上的方法。 // Object.assign方法可以很方便地一次向类添加多个方法 Object.assign(Point.prototype, { toString(){}, toValue(){} }); // constructor方法在new命令执行时自动被调用 // constructor方法默认返回实例对象,即this // 类必须使用new调用,否则会报错 class Point { } // 等同于 class Point { constructor() {} } p.hasOwnProperty('x') // true p.hasOwnProperty('y') // true p.hasOwnProperty('toString') // false p.__proto__.hasOwnProperty('toString') // true p.__proto__.hasOwnProperty('x') // false // 与 ES5 一样,类的所有实例共享一个原型对象 var p1 = new Point(2,3); var p2 = new Point(3,2); p1.__proto__ === p2.__proto__ === Point.prototype //true var p1 = new Point(2,3); var p2 = new Point(3,2); // p1.__proto__.printName = function () { return 'Oops' }; Object.getPrototypeOf(obj).printName = function () { return 'Oops' }; p1.printName() // "Oops" p2.printName() // "Oops" var p3 = new Point(4,2); p3.printName() // "Oops" // 存值函数和取值函数是设置在属性的 Descriptor 对象上的。 var descriptor = Object.getOwnPropertyDescriptor( Point.prototype, "prop" ); "get" in descriptor // true "set" in descriptor // true # 类和模块的内部,默认就是严格模式 # 类不存在变量提升 # Point.name // 'Point' # *[Symbol.iterator] () {...} # 类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。 class Logger { printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); } } const logger = new Logger(); const { printName } = logger; printName(); // TypeError: Cannot read property 'print' of undefined # 一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。 class Logger { constructor() { this.printName = this.printName.bind(this); } // ... } # 另一种解决方法是使用箭头函数。 # 箭头函数内部的this总是指向定义时所在的对象。 class Obj { constructor() { this.getThis = () => this; } } const myObj = new Obj(); myObj.getThis() === myObj // true # 还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this。 function selfish (target) { const cache = new WeakMap(); const handler = { get (target, key) { const value = Reflect.get(target, key); if (typeof value !== 'function') { return value; } if (!cache.has(value)) { cache.set(value, value.bind(target)); } return cache.get(value); } }; const proxy = new Proxy(target, handler); return proxy; } const logger = selfish(new Logger()); # 私有方法和私有属性 # 一种方法就是命名时前面加_,只便于阅读 # 另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。 class Widget { foo (baz) { bar.call(this, baz); } // ... } function bar(baz) { // bar即为私有方法 return this.snaf = baz; } # 还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。 # bar和snaf都是Symbol值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。 const bar = Symbol('bar'); const snaf = Symbol('snaf'); export default class myClass{ // 公有方法 foo(baz) { this[bar](baz); } // 私有方法 [bar](baz) { return this[snaf] = baz; } // ... };
ES6继承 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 38 39 40 41 ES6 的继承机制实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。 class ColorPoint extends Point { } // 等同于 class ColorPoint extends Point { constructor(...args) { super(...args); // 相当于 // Point.prototype.constructor.call(this, ...args) // super作为对象时,在普通方法中,指向父类的原型对象(ParentClass.prototype);在静态方法中,指向父类。 // 这里需要注意,在普通方法中,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性(this.prop=xxx),是无法通过super调用的。 // ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例 // 由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。 // 另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。 // 注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。 } } # 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。 # 父类的静态方法也会被子类继承 # 判断继承关系 Object.getPrototypeOf(ColorPoint) === Point // true class A { constructor() { this.x = 1; } } class B extends A { constructor() { super(); this.x = 2; super.x = 3; console.log(super.x); // undefined, 实际为A.prototype.x console.log(this.x); // 3 } } let b = new B();
ES6类的prototype
属性和__proto__
属性 Class 作为构造函数的语法糖,同时有prototype
属性和__proto__
属性,因此同时存在两条继承链。
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
1 2 3 4 5 6 7 8 9 class A { } class B extends A { } B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true B.prototype // {constructor: class B, __proto__: class A} A.prototype // {constructor: class A, __proto__: Object}
这样的结果是因为,类的继承是按照下面的模式实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class A { } class B { } // B 的实例继承 A 的实例 Object.setPrototypeOf(B.prototype, A.prototype); // B 继承 A 的静态属性 Object.setPrototypeOf(B, A); const b = new B(); Object.setPrototypeOf = function (obj, proto) { obj.__proto__ = proto; return obj; }
这两条继承链,可以这样理解:
作为一个对象,子类(B)的原型(__proto__
属性)是父类(A); 作为一个构造函数,子类(B)的原型对象(prototype
属性)是父类的原型对象(prototype
属性)的实例。 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 class B extends A { } 上面代码的A,只要是一个有prototype属性的函数,就能被B继承。 由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。 讨论2种特殊情况 第一种,子类继承Object类。 class A extends Object { } A.__proto__ === Object // true A.prototype.__proto__ === Object.prototype // true 这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。 第二种情况,不存在任何继承。 class A { } A.__proto__ === Function.prototype // true A.prototype.__proto__ === Object.prototype // true 这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。
ES6子类的实例的__proto__
属性 子类实例的__proto__
属性的__proto__
属性,指向父类实例的__proto__
属性。也就是说,子类的原型的原型,是父类的原型。
因此,通过子类实例的__proto__
.__proto__
属性,可以修改父类实例的行为。
1 2 3 4 5 var p1 = new Point(2, 3); var p2 = new ColorPoint(2, 3, 'red'); p2.__proto__ === p1.__proto__ // false p2.__proto__.__proto__ === p1.__proto__ // true
原生构造函数的继承 原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
1 2 3 4 5 6 7 8 9 Boolean() Number() String() Array() Date() Function() RegExp() Error() Object()
以前,这些原生构造函数是无法继承的。 原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。
ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。
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 class VersionedArray extends Array { constructor() { super(); this.history = [[]]; } commit() { this.history.push(this.slice()); } revert() { this.splice(0, this.length, ...this.history[this.history.length - 1]); } } var x = new VersionedArray(); x.push(1); x.push(2); x // [1, 2] x.history // [[]] x.commit(); x.history // [[], [1, 2]] x.push(3); x // [1, 2, 3] x.history // [[], [1, 2]] x.revert(); x // [1, 2]
Mixin模式的实现 Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。
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 function mix(...mixins) { class Mix { constructor() { for (let mixin of mixins) { copyProperties(this, new mixin()); // 拷贝实例属性 } } } for (let mixin of mixins) { copyProperties(Mix, mixin); // 拷贝静态属性 copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性 } return Mix; } function copyProperties(target, source) { for (let key of Reflect.ownKeys(source)) { if ( key !== 'constructor' && key !== 'prototype' && key !== 'name' ) { let desc = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, desc); } } } class DistributedEdit extends mix(Loggable, Serializable) { // ... }