- 在JS有史以来的大部分时间内,迭代对象属性都是一个难题。
- ES2017新增了两个静态方法,用于将对象内容转换为序列化的————更重要的是可迭代的————格式
- Object.values()和Obejct.entries()都接收一个对象,返回它们的内容的数组。
- Object.values()返回的是对象值得数组,
- Object.entries()返回键值对数组
const o = {
foo: 'bar',
baz: 1,
qux: {}
};
Object.values(o); // ['bar', 1, {}]
Object.entries(o); // [['foo', 'bar'], ['baz', 1], ['qux', {}]]
- 【注意】非字符串属性会被转换为字符串输出。另外,这两个方法执行对象的浅复制。
- 符号属性会被忽略
- 其他原型语法
- 每次定义一个属性或方法都会把Person.prototype重写一遍。为了减少代码冗余,也为了从视觉上更好地封装原型功能,直接通过一个包含所有属性和方法的对象字面量来重写原型成为了一种常见的做法。
function Person () {}
Person.prototype = {
name: 'luke',
age: 29,
job: 'SE',
sayName() {
//...
}
};
-
这样的写法,只有一个问题。就是Person.prototype的constructor属性不指向Person了。上面的写法相当于重写了prototype对象,因此constructor属性也就指向了安全不同的新对象(Object构造函数),不再指向原来的构造函数。
-
虽然instanceof操作符还能可靠地返回值,但我们不能再依靠constructor属性来识别类型了.
-
如果constructor的值很重要,为了避免上述问题,我们重写时要专门设置constructor的值。
function Person () {}
Person.prototype = {
constructor: Person,
name: 'luke',
age: 29,
job: 'SE',
sayName() {
//...
}
};
- 但要注意,以这种方式恢复constructor属性会创建一个
[[Enumerable]]
为true的属性。 - 而原生constructor属性默认是不可枚举的。
- 因此,如果你使用的是兼容ECMAScript的JavaScript引擎,那可能会改为使用Object.defineProperty()方法来定义constructor属性.
function Person () {}
Person.prototype = {
name: 'luke',
age: 29,
job: 'SE',
sayName() {
//...
}
};
// 恢复constructor属性
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
- 原型的动态性
- 因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
let friend = new Person();
Person.prototype.sayHi = function () {
console.log('hi')
}
friend.sayHi(); // hi
-
之所以会这样,主要原因是实例与原型之间松散的联系。
-
在调用friend.sayHi()时,首先会从这个实例中搜索名为sayHi的属性。在没有找到的情况下,运行时会继续搜索原型对象。因为实例和原型之间的链接就是简单的指针,而不是保存的副本,所以会在原型上找到sayHi属性并返回这个属性保存的函数.
-
重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。记住,实例只有指向原型的指针,没有指向构造函数的指针。
function Person () {}
let friend = new Person();
Person.prototype = {
constructor: Person. // 这里的constructor是可枚举的
name: 'luke',
age: 29,
sayName() {
// ...
}
};
friend.sayName(); // error
-
上例中,friend实例是在重写原型对象之前创建的。调用friend.sayName()的时候会导致错误。这是因为friend指向的原型还是最初的原型,而这个原型并没有syaName属性。
-
重写构造函数上的原型之后再创建的实例才会引用新的原型。而在此之前的实例仍然会引用最初的原型。
- 原生对象原型
-
原型模式也实现了所有原生引用类型的模式。所有原生引用类型的构造函数都在原型上定义了实例方法。
-
Object
-
Array,例如sort()就定义在Array.prototype上
-
String,例如substring()定义在String.prototype上
-
等
-
通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。可以像修改自定义对象原型一样修改原生对象原型,因此随时可以添加方法。
// 给String包装类的实例添加一个startsWith()方法
String.prototype.startsWith = function (text) {
return this.indexOf(text) === 0;
};
let msg = "hello world!";
msg.startsWith('hello'); // true
- 不建议自接修改原生对象原型,而是创建一个自定义的类,继承原生类型。在自定义的类的基础上修改。
- 原型的问题
- 1.弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性。
- 2.最主要的问题源自它的共享特性
- 原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外原始值的属性也还好,如前面例子中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。
function Person() {}
Person.prototype = {
constructor: Person,
name: 'luke',
age: 29,
job: 'SE',
friends: ["Shelby", "Court"],
sayName() {
console.log(this.name)
}
};
let p1 = new Person()
let p2 = new Person()
p1.friends.push('Van');
p1.friends; // ['Shelby', 'Court', 'Van']
p2.friendsl // ['Shelby', 'Court', 'Van']
p1.friends === p2.friends; // true
- 但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。