ECMAScript 6 开始正式支持类和继承
构造函数 按照惯例,构造函数名称的首字母 都是大写 的,非构造函数则以小写字母开头
ECMAScript 中的函数是对象 ,每次定义函数时,都会初始化一个对象
使用 new 操作符调用构造函数创建对象实例会执行如下操作:
(1) 在内存中创建一个新对象
(2) 新对象内部的 [[Protoype]] 特性被赋值为构造函数的 prototype 属性
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
(4) 执行构造函数内部的代码(给新对象添加属性)
(5) 构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
instanceof 操作符用于确定对象类型1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function Person (name, age, job ) { this .name = name; this .age = age; this .job = job; this .sayName = function ( ) { console .log (this .name ); }; }let person1 = new Person ("Nicholas" , 29 , "Software Engineer" );let person2 = new Person ("Greg" , 27 , "Doctor" );console .log (person1 instanceof Object ); console .log (person1 instanceof Person ); console .log (person2 instanceof Object ); console .log (person2 instanceof Person );
实例化不传参数时,构造函数后面的括号可加可不加。只要有 new 操作符,就可以调用相应的构造函数 1 2 3 4 5 6 7 8 9 function Person ( ) { this .name = "Jake" ; this .sayName = function ( ) { console .log (this .name ); }; }let person1 = new Person ();let person2 = new Person ;
构造函数也是函数
任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数
没有使用 new 操作符调用构造函数,结果会将属性和方法添加到 window 对象
在调用一个函数没有明确设置 this 值(没有作为对象的方法调用,或没有使用 call()/apply() 调用),this 始终指向 Global 对象(浏览器中为 window 对象)
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 window = global function Person (name, age, job ) { this .name = name; this .age = age; this .job = job; this .sayName = function ( ) { console .log (this .name ); }; this .sayName1 = new Function ("console.log(this.name)" ); }let person = new Person ("Nicholas" , 29 , "Software Engineer" ); person.sayName (); Person ("Greg" , 27 , "Doctor" );window .sayName (); let o = new Object ();Person .call (o, "Kristen" , 25 , "Nurse" ); o.sayName ();
原型模式 每个函数都会创建一个 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 function Person ( ) {}Person .prototype .name = "Nicholas" ;Person .prototype .age = 29 ;Person .prototype .job = "Software Engineer" ;Person .prototype .sayName = function ( ) { console .log (this .name ); };let person1 = new Person (); person1.sayName (); let person2 = new Person (); person2.sayName (); console .log (person1.sayName == person2.sayName ); console .log (Person .prototype .constructor );console .log (Object .getPrototypeOf (person1));console .log (Object .getPrototypeOf (person2));
概念理解 只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指向与之关联的构造函数
前面代码 中的 Person.prototype.constructor 指向 Person
自定义构造函数时,原型对象默认只会获得 constructor 属性,其它所有方法都继承自 Object
每次调用构造函数创建一个新实例,该实例的内部 [[Prototype]] 指针就会被赋值为构造函数的原型对象。浏览器会在每个对象上暴露可以访问原型对象的 __proto__ 属性
实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有联系
构造函数通过 prototype 指向原型对象,实例通过 __proto__ 关联原型对象
Person.prototype 指向原型对象,而 Person.prototpye.constructor 指向 Person 构造函数
原型对象包含 constructor 属性和其他后来添加的属性
两个实例都没有属性和方法,但可以正常调用 sayName() 函数
使用 Object.getPrototypeOf() 可以取得一个对象的原型
为避免使用 Object.setPrototypeOf() 可能造成的性能下降,可以通过 Object.create() 创建一个新对象,同时为其指定原型
1 2 3 4 5 6 7 8 9 10 let biped = { numLegs : 2 };let person = Object .create (biped); person.name = 'Matt' ;console .log (person.name ); console .log (person.numLegs ); console .log (Object .getPrototypeOf (person) === biped);
层级 通过对象访问属性时,如果在对象实例上发现了给定的名称,则返回该名称对应的值。否则,会沿着指针进入原型对象,在原型对象上找到属性,再返回对应的值
只要给对象实例添加一个属性,这个属性就会遮蔽 原型对象上的同名属性。使用 delete 操作符可以完全删除实例上的属性,让标识符解析过程能够继续搜索原型对象
hasOwnProperty() 方法会在属性存在于调用它的对象实例上时返回 true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Person ( ) {}Person .prototype .name = "Nicholas" ;Person .prototype .age = 29 ;Person .prototype .job = "Software Engineer" ;Person .prototype .sayName = function ( ) { console .log (this .name ); };let person1 = new Person ();let person2 = new Person (); person1.name = "Greg" ;console .log (person1.name ); console .log (person2.name ); console .log (person1.hasOwnProperty ("name" )); delete person1.name ;console .log (person1.name ); console .log (person1.hasOwnProperty ("name" ));
ECMAScript 的 Object.getOWnPropertyDescriptor() 方法只对实例属性有效
必须直接在原型对象上调用 Object.getOwnPropertyDescriptor() 获得原型属性的描述符
in 操作符in 操作符会在可以通过对象访问指定属性时返回 true
1 2 3 4 5 6 7 8 9 10 11 12 13 function Person ( ) {}Person .prototype .name = "Nicholas" ;Person .prototype .age = 29 ;Person .prototype .job = "Software Engineer" ;Person .prototype .sayName = function ( ) { console .log (this .name ); };let person1 = new Person ();console .log (person1.hasOwnProperty ("name" )); console .log ("name" in person1);
要确定原型上是否存在某个属性,可以同时使用 hasOwnProperty() 和 in 操作符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function hasPrototypeProperty (object, name ) { return !object.hasOwnProperty (name) && (name in object); }function Person ( ) {}Person .prototype .name = "Nicholas" ;Person .prototype .age = 30 ;Person .prototype .job = "Software Engineer" ;Person .prototype .sayName = function ( ) { console .log (this .name ); };let person = new Person ();console .log (hasPrototypeProperty (person, "name" )); person.name = "Greg" ;console .log (hasPrototypeProperty (person, "name" ));
Object.keys() 可以获得对象上所有可枚举的实例属性
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 ( ) {}Person .prototype .name = "Nicholas" ;Person .prototype .age = 29 ;Person .prototype .job = "Software Engineer" ;Person .prototype .sayName = function ( ) { console .log (this .name ); };let keys = Object .keys (Person .prototype );console .log (keys); let p1 = new Person (); p1.name = "Rob" ; p1.age = 50 ;let p1keys = Object .keys (p1);console .log (p1keys); let keys1 = Object .getOwnPropertyNames (Person .prototype );console .log (keys1);
Object.geOwnPropertySymbols() 用于获取对象自身所有 Symbol 类型属性 (不包含继承的 Symbol 属性),与 Object.getOwnPropertyNames() 类似,但专门针对 Symbol 类型的属性(普通字符串属性不会被返回)
属性枚举顺序 for-in 循环和 Object.keys() 的枚举顺序时不确定的,取决于 JavaScript 引擎,可能因浏览器而异
Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() 和 Object.assign() 先以升序枚举数值键 ,然后以插入顺序枚举字符串和符号键 。
在对象字面量中定义的键以它们逗号分割的顺序(键值对的书写顺序 )插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let k1 = Symbol ('k1' ), k2 = Symbol ('k2' );let o = { 1 : 1 , first : 'first' , [k2]: 'sym2' , second : 'second' , 0 : 0 }; o[k1] = 'sym2' ; o[3 ] = 3 ; o.third = 'third' ; o[2 ] = 2 ;console .log (Object .getOwnPropertyNames (o));console .log (Object .getOwnPropertySymbols (o));
对象迭代 静态方法 Object.values() 和 Object.entries() 用于将对象内容转换为可迭代序列化的格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const sym = Symbol ();const o = { foo : 'bar' , baz : 1 , qux : {}, [sym]: 'foo' };console .log (Object .values (o));console .log (Object .entries (o));console .log (Object .values (o)[0 ] === o.foo ); console .log (Object .entries (o)[0 ][1 ] === o.foo );
其他原型语法 为了减少代码冗余,通常直接通过一个包含所有属性和方法的对象字面量来重写原型
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 Person ( ) {}Person .prototype = { constructor : Person , name : "Nicholas" , age : 29 , job : "Software Engineer" , sayName ( ) { console .log (this .name ); } };let friend = new Person ();console .log (friend instanceof Object ); console .log (friend instanceof Person ); console .log (friend.constructor == Person ); console .log (friend.constructor == Object );
原型动态性 从原型上搜索值的过程是动态的,即使实例在修改原型之前已经存在,任何时候对原型所作的修改也会在实例上反映出来
原因:实例和原型之间使用指针链接,而不是保存的副本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Person ( ) {}Person .prototype = { name : "Nicholas" , age : 29 , job : "Software Engineer" , sayName ( ) { console .log (this .name ); } };let friend = new Person ();Person .prototype .sayHi = function ( ) { console .log ("hi" ); }; friend.sayHi ();
重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function Person ( ) {}let friend = new Person ();Person .prototype = { constructor : Person , name : "Nicholas" , age : 29 , job : "Software Engineer" , sayName ( ) { console .log (this .name ); } }; friend.sayName ();
原生对象原型 所有原生引用类型的构造函数(包括 Object、Array、String 等)都在原型上定义了实例方法
可以像修改自定义对象原型一样修改原生对象原型,随时可以添加方法
1 2 3 4 5 6 7 8 9 10 String .prototype .startsWith = function (text ) { return this .indexOf (text) === 0 ; };let msg = "Hello world!" ;console .log (msg.startsWith ("Hello" ));
推荐创建一个自定义的类,继承原生类型 不推荐修改原生对象原型,直接修改原生对象类型可能引发命名冲突
原型的问题 原型弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值
原型最主要问题源自它的共享特性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Person ( ) {}Person .prototype = { constructor : Person , name : "Nicholas" , age : 29 , job : "Software Engineer" , friends : ["Shelby" , "Court" ], sayName ( ) { console .log (this .name ); } };let person1 = new Person ();let person2 = new Person (); person1.friends .push ("Van" );console .log (person1.friends ); console .log (person2.friends ); console .log (person1.friends === person2.friends );
由于 friends 属性存在于 Person.prototype 而非 person1 上,新加的 “Van” 也会在 person2.friends 上反映出来
一般不同的实例应该有属于自己的属性副本,所以在实际开发中通常不单独使用原型模式