原型对象 理解原型
每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向函数的原型对象,使用原型对象可以让所有对象实例共享它所包含的属性和方法
所有函数的原型对象都会自动获得一个 constructor(构造函数)属性,这个属性指向 prototype 属性所在函数(Person.prototype.constructor -> Person)
构造函数的原型对象只会默认取得 constructor 属性,其他方法都是从 Object 继承而来的
当调用构造函数创建一个新实例后,该实例的内部包含一个指针(内置属性 [[prototype]]),使用 __proto__ 属性可以访问到这个内置属性,__proto__ 指向构造函数的原型对象(p1.__proto__ -> Person.prototype)
1 2 3 4 5 6 7 8 9 10 11 function Person ( ) {} Person .prototype .name = 'Max' ;Person .prototype .age = 27 ;Person .prototype .hobbies = ['swimming' , 'jogging' ];Person .prototype .sayName = function ( ) { console .log (this .name ); } const p1 = new Person ();
Object.setPrototypeOf() 方法传入两个对象作为参数,将第二个对象设置为第一个对象的原型
1 2 3 4 5 6 7 8 9 10 11 const person = {}; const someone = { name : 'Albert' , sayHi : function ( ) { console .log ('Hi~' + this .name ); } }; Object .setPrototypeOf (person, someone);
Object.getPrototypeOf() 方法用于获取对象的原型
1 2 3 Object .getPrototypeOf (p1).name Object .getPrototypeOf (person).name Object .getPrototypeOf (person).sayHi ()
为实例添加一个与原型同名的属性,该属性会屏蔽(不是修改)原型中的同名属性
1 2 3 4 5 const p2 = new Person ();p2.name = 'Anthony' ; console .log (p1.name ); console .log (p2.name );
使用 hasOwnProperty() 方法可以判断一个属性是存在于实例中,还是存在于原型中,当给定属性存在于对象实例中时,返回 true,否则返回 false
1 2 console .log (p1.hasOwnProperty ('name' )); console .log (p2.hasOwnProperty ('name' ));
遍历对象属性的方法 有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用
在单独使用 in 时,in 操作符只要通过对象能够访问给定属性就会返回 true,无论该属性存在于实例中还是原型中
1 2 3 4 5 6 7 console .log (p1.hasOwnProperty ('name' )); console .log ('name' in p1); console .log (p2.hasOwnProperty ('name' )); console .log ('name' in p2);
在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的属性(enumerable: true),其中既包括存在于实例中的属性,也包括存在于原型中的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function Person ( ) { this .name = 'Max' ; } Person .prototype .age = 27 ;const p1 = new Person ();Object .defineProperty (p1, 'hobbies' , { value : ['swimming' , 'jogging' ], enumerable : false }) for (let prop in p1) { console .log (prop); }
使用 Object.key() 方法遍历对象所有可枚举的实例属性(不遍历原型中的属性),返回一个包含所有可枚举属性的字符串数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function Person ( ) { this .name = 'Max' ; } Person .prototype .age = 27 ;const p1 = new Person ();Object .defineProperty (p1, 'hobbies' , { value : ['swimming' , 'jogging' ], enumerable : true }) Object .defineProperty (p1, 'job' , { value : 'software engineer' , enumerable : false }) let keys = Object .keys (p1);console .log (keys);
使用 Object.getOwnPropertyNames() 方法遍历所有实例属性(不遍历原型中的属性),无论实例属性是否可枚举,返回一个包含所有可枚举属性的字符串数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function Person ( ) { this .name = 'Max' ; } Person .prototype .age = 27 ;const p1 = new Person ();Object .defineProperty (p1, 'hobbies' , { value : ['swimming' , 'jogging' ], enumerable : true }) Object .defineProperty (p1, 'job' , { value : 'software engineer' , enumerable : false }) let keys = Object .getOwnPropertyNames (p1);console .log (keys);
更简洁的原型语法
通过使用 constructor 属性可以访问创建该对象时所用的函数
所有实例对象都可以使用 constructor 属性验证其原始类型
1 2 3 4 5 6 7 8 9 10 11 12 13 function Person ( ) {} const p = new Person ();console .log (typeof p === 'object' ); console .log (p instanceof Person ); console .log (p.constructor === Person );
为了精简代码,可以使用一个包含所有属性和方法的对象字面量来重写原型对象
1 2 3 4 5 6 7 8 9 10 11 function Person ( ) {} Person .prototype = { name : 'Max' , age : 27 , hobbies : ['swimming' , 'jogging' ], sayName : function ( ) { console .log (this .name ); } } const p1 = new Person ();
但是! 由于 Person.prototype 是一个以对象字面量形式创建的新对象,因此 constructor 属性不再指向 Person,而是指向了 Object
1 2 3 4 console .log (p1 instanceof Person ); console .log (p1 instanceof Object ); console .log (p1.constructor === Person ); console .log (p1.constructor === Object );
使用 Object.defineProperty() 方法修改 constructor 属性的配置
1 2 3 4 5 6 Object .defineProperty (Person .prototype , 'constructor' , { enumerable : false , value : Person }) console .log (p1.constructor === Person );
原型的动态性
可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Person ( ) { this .name = 'Max' ; } const p1 = new Person ();Person .prototype .age = 24 ;Person .prototype .sayName = function ( ) { return this .name ; }; console .log (p1.age ); console .log (p1.sayName ());
但是,如果重写了整个原型对象,已经创建的实例仍然指向原先的原型
1 2 3 4 5 6 7 8 9 10 Person .prototype = { showHobbies : function ( ) { return ['swimming' , 'jogging' ]; } }; console .log (p1.showHobbies ); console .log (p1.sayName ());
1 2 3 4 const p2 = new Person ();console .log (p2.showHobbies ()); console .log (p2.sayName );
原型对象的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function Person ( ) {} Person .prototype = { constructor : Person , name : 'Max' , age : 24 , hobbies : ['swimming' , 'jogging' ] } const p1 = new Person ();p1.hobbies .push ('singing' ); const p2 = new Person ();console .log (p1.hobbies ); console .log (p2.hobbies ); console .log (p1.hobbies === p2.hobbies );
构造函数与原型 组合使用构造函数模式和原型模式可以解决上述原型对象的问题,其中
构造函数模式用于定义实例属性
原型模式用于定义方法和共享的属性
因此,每个实例都会有自己的一份实例属性副本,同时又共享着对方法的引用,节省了内存,还可以向构造函数传递参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function Person (name, age ) { this .name = name; this .age = age; this .hobbies = ['swimming' , 'jogging' ]; } Person .prototype = { constructor : Person , sayName : function ( ) { return this .name ; } } const p1 = new Person ('Max' , 27 );p1.hobbies .push ('singing' ); const p2 = new Person ('Anthony' , 24 );console .log (p1.hobbies ); console .log (p2.hobbies ); console .log (p1.hobbies === p2.hobbies ); console .log (p1.__proto__ === Person .prototype ); console .log (p2.__proto__ === Person .prototype );
把函数作为构造函数,通过 new 进行调用时,this 指向新创建的对象实例,所以在构造函数内部添加的属性直接赋给新创建的实例
当通过实例访问构造函数内部的属性时,不需要遍历原型链
1 2 3 4 5 6 7 8 9 10 11 12 13 function Person ( ) { this .flag = false ; this .getFlag = function ( ) { return !this .flag ; } } Person .prototype .getFlag = function ( ) { return this .flag ; } const person = new Person ();person.getFlag ();
实现继承
由于 JS 中的函数没有签名,因此 JS 只支持实现继承方式,不支持接口继承方式
原型链是实现继承的主要方法
原型链
原型链的基本思想是,利用原型让一个引用类型继承另一个引用类型的属性和方法
令原型对象等于另一个原型的实例(SubType.prototype = new SuperType()),则此时的原型对象将包含一个指向另一个原型的指针(SubType.prototype.__proto__ -> SuperType.prototype),相应地,另一个原型中也包含着一个指向另一个构造函数的指针(SuperType.prototype.constructor -> SuperType),以此类推,形成一个原型链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function SuperType ( ) { this .job = 'Artist' ; } SuperType .prototype .getJob = function ( ) { return this .job ; } function SubType ( ) { this .name = 'Anthony' ; } SubType .prototype = new SuperType ();SubType .prototype .getName = function ( ) { return this .name ; } const instance = new SubType ();console .log (instance.getJob ());
通用原型链实现继承时,查找特定属性将会被委托在整个原型链上,只有当没有更多的原型可以进行查找时,才会停止查找
所有函数的默认原型都是 Object 的实例,默认原型内部都会指向 Object.prototype,因此所有自定义类型都会继承 toString()、valueOf() 等默认方法
确定原型和实例的关系
方法一:instanceof 运算符用于检测(右侧的)函数的原型是否存在于(左侧的)实例对象的原型链中
1 2 3 instance instanceof SubType instance instanceof SuperType instance instanceof Object
方法二:isPrototypeOf() 方法用于检查一个对象是否存在于另一个对象的原型链中
1 2 3 SubType .prototype .isPrototypeOf (instance) SuperType .prototype .isPrototypeOf (instance) Object .prototype .isPrototypeOf (instance)
谨慎地定义方法
当子类重写超类中的某个方法,或者子类添加超类中不存在的某个方法时,给子类的原型添加方法的代码必须放在替换原型的语句之后
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 function SuperType ( ) { this .job = 'Artist' ; } SuperType .prototype .getJob = function ( ) { return this .job ; } function SubType ( ) { this .name = 'Anthony' ; } SubType .prototype = new SuperType ();SubType .prototype .getName = function ( ) { return this .name ; } SubType .prototype .getJob = function ( ) { return 'singer' ; } const instance = new SubType ();console .log (instance.getJob ()); const instance2 = new SuperType ();console .log (instance2.getJob ());
通过原型实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链
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 function SuperType ( ) { this .job = 'Artist' ; } SuperType .prototype .getJob = function ( ) { return this .job ; } function SubType ( ) { this .name = 'Anthony' ; } SubType .prototype = new SuperType ();SubType .prototype = { getName : function ( ) { return this .name ; }, getHobby : function ( ) { return 'singing' ; } } const instance = new SubType ();console .log (instance.getJob ()); console .log (SubType .prototype .constructor === Object );
原型链的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function SuperType ( ) { this .hobbies = ['swimming' , 'jogging' ]; } function SubType ( ) { } SubType .prototype = new SuperType ();const instance1 = new SubType ();instance1.hobbies .push ('singing' ); console .log (instance1.hobbies ); const instance2 = new SubType ();console .log (instance2.hobbies );
问题二:在创建子类的实例时,不能向超类的构造函数传递参数
1 2 3 4 5 6 7 8 9 10 function SuperType (name ) { this .name = name; } function SubType ( ) {} SubType .prototype = SuperType ();const instance = new SubType ('Max' );console .log (instance.name );
鉴于以上问题,因此很少单独使用原型链,而是借助构造函数进行组合式继承。
组合继承
通过原型链,即子类的原型指向超类的实例,实现对原型的属性和方法的继承(共享)
借用构造函数,即在子类构造函数的内部使用 call() 或 apply() 方法调用超类构造函数,将超类的实例属性绑定到子类的 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 30 31 32 33 34 35 36 37 38 39 40 41 function SuperType (name ) { this .name = name; this .hobbies = ['swimming' , 'jogging' ]; this .sayHi = function ( ) { console .log ('Hi~' + this .name ); } } SuperType .prototype .sayName = function ( ) { return this .name ; } function SubType (name, age ) { SuperType .call (this , name); this .age = age; } SubType .prototype = new SuperType (); SubType .prototype .constructor = SubType ;SubType .prototype .sayAge = function ( ) { return this .age ; } const instance1 = new SubType ('Anthony' , 23 );instance1.hobbies .push ('singing' ); instance1.sayHi (); instance1.sayName (); instance1.sayAge (); console .log (instance1.hobbies ); const instance2 = new SubType ('Max' , 24 );instance2.sayHi (); instance2.sayName (); instance2.sayAge (); console .log (instance2.hobbies ); console .log (instance1.constructor === SubType ); console .log (SubType .prototype .constructor === SubType );
组合继承是最常用的继承模式,但是存在一个问题:无论在什么情况下,都会调用两次超类构造函数,一次在创建子类原型时调用,另一次在创建子类实例时又在子类构造函数内部调用,因此生成了两份实例,造成了不必要的内存开销,影响了性能。
寄生组合式继承 改进了原型链 + 借用构造函数的组合继承中的调用两次超类构造函数的问题,不必为了指定子类的原型而调用超类的构造函数,而是使用超类原型的一个副本。
1 2 3 4 5 6 function inheritPrototype (subType, superType ) { const prototype = Object .create (superType.prototype ); prototype.constructor = subType; subType.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 27 28 29 30 31 32 33 34 35 36 37 38 function SuperType (name ) { this .name = name; this .hobbies = ['swimming' , 'jogging' ]; this .sayHi = function ( ) { console .log ('Hi~' + this .name ); } } SuperType .prototype .sayName = function ( ) { return this .name ; } function SubType (name, age ) { SuperType .call (this , name); this .age = age; } inheritPrototype (SubType , SuperType ); SubType .prototype .sayAge = function ( ) { return this .age ; } const instance1 = new SubType ('Anthony' , 23 );instance1.hobbies .push ('singing' ); instance1.sayHi (); instance1.sayName (); instance1.sayAge (); console .log (instance1.hobbies ); const instance2 = new SubType ('Max' , 24 );instance2.sayHi (); instance2.sayName (); instance2.sayAge (); console .log (instance2.hobbies ); console .log (instance1.constructor === SubType ); console .log (SubType .prototype .constructor === SubType );
寄生组合式继承只调用一次 SuperType 构造函数,因此避免了在 SubType.prototype 上创建不必要的属性,同时原型链保持不变。寄生组合式继承是引用类型最理想的继承范式。
类继承 class 是语法糖 ES6 引入关键字 class,提供更为优雅的创建对象还实现继承的方式,但底层仍然是基于原型的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Person { constructor (name, age ) { this .name = name; this .age = age; this .hobbies = ['swimming' , 'jogging' ]; } sayName ( ) { return `Hi~${this .name} ` ; } } const p1 = new Person ('Anthony' , 24 );p1.hobbies .push ('singing' ); console .log (p1.hobbies ); console .log (p1.sayName ()); const p2 = new Person ('Max' , 21 );console .log (p2.hobbies ); console .log (p2.age );
静态方法
被 static 修饰的属性和方法是静态属性和方法
静态属性和方法只能被类名调用,不能被实例对象调用,同时也不能被子类继承
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 class Person { constructor (name, level ) { this .name = name; this .level = level; } static hi = 'Hi!' ; sayName ( ) { return `${Person.hi} ${this .name} ` ; } static compare (person1, person2 ) { return person1.level - person2.level ; } } const p1 = new Person ('Max' , 5 );const p2 = new Person ('Albert' , 3 );p1.sayName (); p2.sayName (); Person .compare (p1, p2); console .log ('compare' in Person ); console .log ('compare' in p1 || 'compare' in p2); console .log ('sayName' in Person );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Person (name, level ){ this .name = name; this .level = level; this .sayName = function ( ) { console .log (Person .hi + this .name ); } } Person .hi = 'Hi! ' ;Person .compare = function (person1, person2 ) { return person1.level - person2.level ; } const p1 = new Person ('Max' , 5 );const p2 = new Person ('Albert' , 3 );p1.sayName (); Person .compare (p1, p2); console .log ('compare' in Person ); console .log ('compare' in p1 || 'compare' in p2);
实现继承
使用关键字 extends 实现继承
使用关键字 super 调用超类构造函数
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 class SuperType { constructor (name ) { this .name = name; this .hobbies = ['swimming' , 'jogging' ]; } sayHi ( ) { console .log ('hi~' + this .name ); } } class SubType extends SuperType { constructor (name, age ) { super (name); this .age = age; } sayAge ( ) { console .log (this .age ); } } const sub1 = new SubType ('Anthony' , 23 );sub1.hobbies .push ('singing' ); console .log (sub1.hobbies ); sub1.sayHi (); sub1.sayAge (); const sub2 = new SubType ('Max' , 24 );console .log (sub2.hobbies ); const super1 = new SuperType ('Albert' );super1.sayHi (); console .log (super1 instanceof SubType ); console .log ('sayAge' in super1);