这是我阅读《You Don’t Know JavaScript》的过程中,记录下来的读书笔记。
一、new操作的问题
1. 副作用
- 前面提到,new操作的过程中,其实是将构造函数作为实例对象的方法进行执行。
- new操作的目的是产生对象。那么这里就会有一个问题,比如说,在这个构造函数执行的过程中
alert()
什么内容,其实也是会顺利弹出。 - 这也就是产生了副作用,因为这不是我们希望发生的。
代码演示:
function Father() {
this.a = '1'
this.b = '2'
var v = '副作用'
alert(v)
}
var k = new Father()
console.log(k.a) // '1'
console.log(k.v) // undefined
该代码执行过程中,会弹出提示框:
2. 返回值
- 还有一个问题,我们都知道执行构造函数的过程中,为实例对象添加属性和方法。那如果构造函数返回的是一个对象呢?
function Father() {
this.a = '1'
return {
b: '2'
}
}
var k = new Father()
console.log(k.a) // undefined
console.log(k.b) // '2'
- 其实就会将返回的对象做为实例对象。
- 如果返回的不是对象,则不会有这个问题。
二、constructor
1. 位置误解
-
前面说到
constructor
时,原型对象B的constructor
属性指向构造函数A,也就是B.constructor === A
是成立的。这意味着B有constructor
属性。 -
其实不是这样的,
constructor
属性其实在A.prototype
上。所以是A.prototype.constructor
指向构造函数A。 -
constructor的错误理解很容易产生误导。
-
比如:A.prototype的constructor属性只是在函数A在声明时的默认属性。如果你创建了一个新对象,并用这个新对象替换了函数默认的原型对象,那么新对象并不会自动获取constructor属性。
function Foo() {...}
Foo.prototype = {...}// 创建一个新原型对象
var a1 = new Foo()
a1.constructor === Foo;// false
a1.constructor === Object; // true
- 从上面代码可以看出a1是有Foo构造的,那么
a1.constructor
为什么是指向Object
而不是Foo
。而且a1为什么有constructor
属性? - 其实a1并没有constructor属性,所以它会通过原型链上的
Foo.prototype
访问到。如果这个对象也没有constructor
属性。就会继续向上查找,一直到顶层的Object.prototype
。这里就有一个constructor属性,指向内置的Object()函数。
2. 不可靠性
- 当然,你也可以手动给Foo.prototype添加一个constructor属性,不过需要符合正常行为的不可枚举属性。
- 而对象的constructor属性,会默认指向一个函数,这个函数可以找到prototype上的constructor属性。
从上面可以看出,constructor属性是非常不可靠并且不安全的引用。
三、__proto__
1. 作用
绝大多数浏览器支持这一种方法来访问原型对象:
a.__proto__ === Foo.prototype;// true
- 甚至可以直接通过,
__proto__.__proto__...
来遍历原型链 - 和constructor一样,
__proto__
实际上并不存在于对象a中,也是可设置的。
比如:__proto__
存在于Object.prototype
上。
2. 理解
__proto__
看起来很像属性,实际上更像一个getter/setter
它的实现大致是这样的:
Object.defineProperty(Object.prototype, "__proto__") {
get: function() {
return Object.getPrototypeOf(this);
},
set: function() {
Object.setPrototypeOf(this, o);
return o;
}
}
四、对象关联
- 原型链其实就是存在于对象中的一个内部链接,对象可以通过它引用其他对象。
- 这个链接的作用:如果在对象上没有找到需要的属性或方法引用,可以在
__proto__
上关联的对象上继续查找。同理,在后者中也没有找到需要的引用就会继续查找它的__proto__
,以此类推。这一系列对象的链接就成为原型链。
除了前面通过new操作的方式,我们可以使用其他方法关联对象。
1. 复制引用
通过前面的内容,我们知道,原型对象的作用就是共享属性和方法。
- 而且原型对象本身就是对象,构造函数和实例对象中保存的是引用。我们可以将引用直接复制到其他地方。
- 比如new操作里面,将构造函数的
prototype
属性包含的原型对象引用复制给实例对象的__proto__
属性。 - 比如,我们将对象A作为函数B和函数C的原型对象,函数B和函数C进行new操作创建的对象就能访问到原型对象A的属性。
那么,如果我想让函数D创建的对象也能访问到对象A中的属性,那该怎么做?
- 其实很简单,直接赋值操作就可以了,是不是很简单。
D.prototype = B.prototype
- 上面代码中,D的
prototype
属性引用了B的原型对象,也就是两个函数的原型对象是同一个。 - 这种方式很简单,但是我们很少这样做,因为当我们修改
D.prototype
的时候,B.prototype
也会发生变化,也就是B的原型对象也变了。多数情况下,我们是不想出现这种问题。 - 而且,后续还要进行new操作才能产生实例对象。
那么,又要能访问到,又不想影响到,有没有这样的方法呢?是原型链继承,后面会讲到
下面两个方法,就不需要通过构造函数了,直接是建立实例对象与原型对象的关系。
2. Object.create()
该方法的作用是创建一个新对象,使用现有的对象作为新对象的原型对象。这个是比较常用的方法。
Object.create(proto, [propertiesObject])
参数
proto
:必须。表示新建对象的原型对象,即该参数会被赋值到新对象的原型上。
propertiesObject
:可选。 添加到新创建对象的可枚举属性(即其自身的属性,而不是原型链上的枚举属性)对象的属性描述符以及相应的属性名称。
比如:
var foo = {
somthing: function() {
console.log("Tell me something");
}
};
var bar = Object.create(foo);
bar.something(); // Tell me something
Object.create(null) // 创建出没有原型对象的实例对象
优点:没有new操作时,调用构造函数产生的副作用。
缺点:该方法时创建新对象,不能修改既有对象,也就是需要把原有对象替换掉。
Object.create() 的好处
Object.create()
可以直接关联起两个对象,避免了中间不必要的麻烦(比如:new操作)
补充:JS中只有两种创建对象的方法
- 我们知道js中创建一个自定义对象有两种方法,一种是使用new,另一种是使用对象字面量形式。
- 至于构造函数模式、工厂模式、原型模式、组合模式、寄生模式、es6中的class、Object.create()等等,都不过是这两种方法的组合应用。
新建对象需要把原来的废弃掉,那有没有一个在既有对象上修改的方法呢?
Object.setPrototypeOf()
用来设置一个对象的 prototype 对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
Object.setPrototypeOf(object, prototype)
代码示例
var proto = {
y: 20,
z: 40
};
var o = { x: 10 };
Object.setPrototypeOf(o, proto);
五、检查“类”关系
1. instanceof()
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
怎么判断a的“类”?
a instanceof Foo; // true
instanceof
的判断原理是:在a的原型链上,是否有指向Foo.prototype
的对象。
这个方法只能用于实例对象和构造函数之间。
2. isPrototypeOf()
Foo.prototype.isPrototypeOf(a) // true
原理:在a的原型链上是否存在Foo.prototype
在es5中是
Object.getPrototypeOf(a) === Foo.prototype // true