@javascript
ECMAScript 2017 新增了两个静态方法,用于将对象内容转换为序列化的——更重要的是可迭代的——格式。java
Object.values() Object.entries()
Object.values()返回对象值的数组,Object.entries()返回键/值对的数组。数组
const o = { foo: 'bar', baz: 1, qux: {} }; console.log(Object.values(o)); // ["bar", 1, {}] console.log(Object.entries((o))); // [["foo", "bar"], ["baz", 1], ["qux", {}]]
注意,非字符串属性会被转换为字符串输出。另外,这两个方法执行对象的浅复制:app
符号属性会被忽略函数
为了减小代码冗余,也为了从视觉上更好地封装原型功能,直接经过一个包含全部属性和方法的对象字面量来重写原型成为了一种常见的作法。this
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } };
这样重写以后,Person.prototype 的 constructor 属性就不指向 Person了。上面的写法彻底重写了默认的 prototype 对象,所以其 constructor 属性也指向了彻底不一样的新对象(Object 构造函数)。虽然 instanceof 操做符还能可靠地返回值,但咱们不能再依靠 constructor 属性来识别类型了。
若是 constructor 的值很重要,则能够像下面这样在重写原型对象时专门设置一下它的值:prototype
function Person() { } Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } };
原生 constructor 属性默认是不可枚举的。使用 Object.defineProperty()方法来定义constructor 属性3d
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } }; // 恢复 constructor 属性 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
由于从原型上搜索值的过程是动态的,因此即便实例在修改原型以前已经存在,任什么时候候对原型对象所作的修改也会在实例上反映出来。就是先建立实例,而后给原型赋值,因为是指针,因此后赋予的值,在以前创造的对象上也能访问到。指针
虽然随时能给原型添加属性和方法,并可以当即反映在全部对象实例上,但这跟重写整个原型是两回事。实例的[[Prototype]]指针是在调用构造函数时自动赋值的,这个指针即便把原型修改成不一样的对象也不会变。重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。记住,实例只有指向原型的指针,没有指向构造函数的指针。code
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 等)都在原型上定义了实例方法。
经过原生对象的原型能够取得全部默认方法的引用,也能够给原生类型的实例定义新的方法。能够像修改自定义对象原型同样修改原生对象原型,所以随时能够添加方法。
原型的最主要问题源自它的共享特性。
咱们知道,原型上的全部属性是在实例间共享的,这对函数来讲比较合适。
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); // "Shelby,Court,Van" console.log(person2.friends); // "Shelby,Court,Van" console.log(person1.friends === person2.friends); // true
若是这是有意在多个实例间共享数组,那没什么问题。但通常来讲,不一样的实例应该有属于本身的属性副本。这就是实际开发中一般不单独使用原型模式的缘由。
简单来讲就是给一个子类的原型对象用一个父类的实例对象来代替,因此就能够在子类的原型链中访问到父类的属性。
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 继承 SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function () { return this.subproperty; }; let instance = new SubType(); console.log(instance.getSuperValue()); // true
原型链扩展了前面描述的原型搜索机制。咱们知道,在读取实例上的属性时,首先会在实例上搜索这个属性。若是没找到,则会继承搜索实例的原型。在经过原型链实现继承以后,搜索就能够继承向上,搜索原型的原型。
实际上,原型链中还有一环。默认状况下,全部引用类型都继承自 Object,这也是经过原型链实现的。任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指Object.prototype。这也是为何自定义类型可以继承包括 toString()、valueOf()在内的全部默认方法的缘由。
第一种方式是使用 instanceof 操做符。
第二种方式是使用 isPrototypeOf()方法。原型链中的每一个原型均可以调用这个方法。
子类有时候须要覆盖父类的方法,或者增长父类没有的方法。为此,这些方法必须在原型赋值以后再添加到原型上。
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 继承 SuperType SubType.prototype = new SuperType(); // 新方法 SubType.prototype.getSubValue = function () { return this.subproperty; }; // 覆盖已有的方法 SubType.prototype.getSuperValue = function () { return false; }; let instance = new SubType(); console.log(instance.getSuperValue()); // false
另外一个要理解的重点是,以对象字面量方式建立原型方法会破坏以前的原型链,由于这至关于重写
了原型链。
主要问题出如今原型中包含引用值的时候。在使用原型实现继承时,原型实际上变成了另外一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。
原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。事实上,咱们没法在不影响全部对象实例的状况下把参数传进父类的构造函数。
基本思路很简单:在子类构造函数中调用父类构造函数。由于毕竟函数就是在特定上下文中执行代码的简单对象,因此可使用apply()和 call()方法以新建立的对象为上下文执行构造函数。
function SuperType() { this.colors = ["red", "blue", "green"]; } function SubType() { // 继承 SuperType SuperType.call(this); } let instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" let instance2 = new SubType(); console.log(instance2.colors); // "red,blue,green"
相比于使用原型链,盗用构造函数的一个优势就是能够在子类构造函数中向父类构造函数传参。
function SuperType(name){ this.name = name; } function SubType() { // 继承 SuperType 并传参 SuperType.call(this, "Nicholas"); // 实例属性 this.age = 29; } let instance = new SubType(); console.log(instance.name); // "Nicholas"; console.log(instance.age); // 29
能够在调用父类构造函数以后再给子类实例添加额外的属性。
盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,所以函数不能重用。此外,子类也不能访问父类原型上定义的方法,所以全部类型只能使用构造函数模式。因为存在这些问题,盗用构造函数基本上也不能单独使用。
基本的思路是使用原型链继承原型上的属性和方法,而经过盗用构造函数继承实例属性。这样既能够把方法定义在原型上以实现重用,又可让每一个实例都有本身的属性。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age){ // 继承属性 SuperType.call(this, name); this.age = age; } // 继承方法 SubType.prototype = new SuperType(); SubType.prototype.sayAge = function() { console.log(this.age); }; let instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" instance1.sayName(); // "Nicholas"; instance1.sayAge(); // 29 let instance2 = new SubType("Greg", 27); console.log(instance2.colors); // "red,blue,green" instance2.sayName(); // "Greg"; instance2.sayAge(); // 27
组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。并且组合继承也保留了 instanceof 操做符和 isPrototypeOf()方法识别合成对象的能力。