JavaScript忍者秘籍——原型

时间:2022-02-01 05:52:38

概要:本篇博客主要介绍JavaScript的原型

1.对象实例化

- 初始化的优先级

初始化操作的优先级如下:

  ● 通过原型给对象实例添加的属性

  ● 在构造器函数内给对象实例添加的属性

  在构造器内的绑定操作优先级永远都高于在原型上的绑定操作优先级。因为构造器的this上下文指向的是实例自身,所以我们可以在构造器内对核心内容执行初始化操作。

- 协调引用

  如下代码观察原型变化时的行为:

function Ninja(){
this.swung = true;
}
var ninja = new Ninja();
Ninja.prototype.swingSword = function(){
return this.swung;
};
assert(ninja.swingSword(),
"Method exists, even out of order.");

  在本例中,定义了一个构造器Ninja,并使用它创建了一个对象实例。然后 在原型上添加一个方法并运行测试。结果表明:原型上的属性并没有复制到其他地方,而是附加到新创建的对象上了,并可以和对象自身的属性引用一起协调运行。

  JavaScript中的每个对象,都有一个名为constructor的隐式属性,该属性引用的是创建该对象的构造器。由于prototype是构造器的一个属性,所以每个对象都有一种方式可以找到自己的原型。

JavaScript忍者秘籍——原型

  在控制台输入ninja.constructor引用时,我们看到的是所期望的Ninja()函数,这是因为该对象是使用此函数作为构造器进行创建的。一个更深层次的ninja.constructor.prototype.swingSword引用,说明了我们可以通过这种方式访问对象实例的原型属性。

  改变上述示例如下:

function Ninja(){
this.swung = true;
this.swingSword = function(){
return !this.swung;
};
} var ninja = new Ninja();
Ninja.prototype.swingSword = function(){
return this.swung;
};
console.assert(ninja.swingSword(),
"Called the instance method, not the prototype method.");

  在本例中,我们重新定义了一个和原型方法同名的实例方法,并在构造器执行之后才创建原型方法。测试表明,即便是在实例化对象之后,再在原型方法添加方法,实例方法也会优先考虑原型上的该方法。

- 通过构造器判断对象类型

  对象的构造器可以通过constructor属性获得。任何时候我们都可以重新引用该构造器,甚至可以使用它进行类型检查,示例如下:

function Ninja(){}
var ninja = new Ninja();
assert(typeof ninja == "object",
"The type of the instance is object.");
assert(ninja instanceof Ninja,
"instanceof identifies the constructor.");
assert(ninja.constructor == Ninja,
"The ninja object was created by the Ninja function.");

  使用typeof操作符判断该实例的类型,其检查结果并不是很有用,只是告诉我们该实例只是一个对象而已,因为检查结果总是返回object。而instanceof操作符,才是真正有用的,该操作符告诉了我们一个很明确的方法,来确定一个实例是否是由特定的函数构造器所创建。

  除此之外,我们还可以使用constructor属性,我们此时知道,该属性作为创建该对象的原始函数引用,被添加在所有的实例上。利用该属性,我们可以验证实例的起源。

function Ninja(){}
var ninja = new Ninja();
var ninja2 = new ninja.constructor();
assert(ninja2 instanceof Ninja, "It's a Ninja!");
assert(ninja !== ninja2, "But not the same Ninja!");

  定义一个构造器,然后使用该构造器创建一个实例。接着使用该实例的constructor属性再创建第二个实例。测试结果表明,成功创建了第二个Ninja对象,并且该变量指向的并不是同一个实例。

- 继承与原型链

  instanceof 操作符的另一个功能是作为对象继承的一种形式。如下代码:使用原型实现继承

function Person(){}
Person.prototype.dance = function(){};
function Ninja(){}
Ninja.prototype = {dance:Person.prototype.dance};
var ninja = new Ninja();
assert(ninja instanceof Ninja,
"ninja receives functionality from the Ninja prototype");
assert(ninja instanceof Person,"... and the Person prototype");
assert(ninja instanceof Object,"... and the Object prototype");

  由于函数的原型只是一个对象,所以有很多功能可以通过复制的方法达到继承的目的。在上述代码中,首先定义一个Person,然后再定义一个Ninja。因为Ninja显然也是一个人,所以我们希望Ninja也能继承Person的属性。我们尝试将Person原型的dance属性方法复制到Ninja的原型上作为同名方法来实现继承功能。测试结果表明,这是复制而不是继承。

  我们真正想要实现的是一个原型链,创建这样一个原型链最好的方式是,使用一个对象的实例作为另外一个对象的原型:SubClass.prototype = new SuperClass();

例如:Ninja.prototype = new Person();这将保持原型链,因为SubClass实例的原型将是SuperClass的一个实例,该实例不仅拥有原型,还持有SuperClass的所有属性,并且该原型指向其自身超类的一个实例。

function Person(){}
Person.prototype.dance = function(){};
function Ninja(){}
Ninja.prototype = new Person();
var ninja = new Ninja();
assert(ninja instanceof Ninja,
"ninja receives functionality from the Ninja prototype");
assert(ninja instaceof Person, "... and the Person prototype");
assert(typeof ninja.dance == "function","... and can dance!");

  上述代码将Person的一个实例作为Ninja的原型。通过执行instanceof操作,可以判断函数是否继承了其原型链中任何对象的功能。这种原型继承方式还有一个比较好的副作用是,所有原型中继承的函数都是实时更新的。

  所有原声JavaScript对象构造器(如Object、Array、String、Number、RegExp、Function)都有可以被操作和扩展的原型属性,这是有道理的,因为每个对象构造器自身就是一个函数。

2. 疑难陷阱

- 扩展对象

  我们也许会犯的及其严重的错误就是去扩展原生Object.prototype。其原因是,在扩展该原型时,所有的对象都会接受这些额外的属性。在我们遍历对象属性的时候,这个问题特别严重,新属性的出现可能会导致各种意想不到的行为。

object.prototype.keys = function(){
var keys = [];
for (var p in this) keys.push(p);
return keys;
};
var obj = {a: 1, b: 2, c: 3};
assert(obj.keys().length == 3,
"There are three prototypes in this object.");

  JavaScript提供一个名为hasOwnProperty()的方法,使用该方法可以确定一个属性是在对象实例上定义的,还是从原型里导入的。

Object.prototype.keys = function(){
var keys = [];
for (var i in this)
if (this.hasOwnProperty(i)) keys.push(i);
return keys;
};
var obj = {a: 1, b: 2, c: 3};
assert(obj.keys().length == 3,
"There are three properties in this object.");

  我们重新定义了方法,忽略了非实例属性。

- 扩展数字

  除了我们在上一节研究的Object,对其他原声对象的原型进行扩展相对来说比较安全。但是,另外一个有问题的原生对象是Number。

  在Number原型上添加一个方法

Number.prototype.add = function(num){
return this + num;
}
var n = 5;
assert(n.add(3) == 8,
"It works when the number is in a variable.");
assert((5).add(3) == 8,
"Also works if a number is wrapped in parentheses.");
assert(5.add(3) == 8,
"What about a simple literal?");

  这里,我们在Number上定义一个新方法add(),该方法接收一个参数,并将该参数与自身数值进行相加,然后再进行返回。然后用各种方式验证该新方法。但是,当我们尝试在浏览器中加载页面时,页面不会加载。一般来说,除非真的需要,最好避免在Number的原型上做扩展。

- 实例化问题

  函数有两种用途:作为"普通"的函数,以及作为构造器。因此容易混淆用法。

function User(first, last){
this.name = first + " " + last;
}
var user = User("Ichigo", "Kurosaki");
assert(user, "User instantiated");
assert(user.name == "Ichigo Kurosaki",
"User name correctly assigned");

  在上述代码中,我们定义一个User类,其构造器接受first和last参数,并将其连接形成一个完整的名称,再赋值给name属性。然后,我们创建一个该类的实例赋值给user变量,并测试该对象是否被实例化,以及构造器的执行是否正确。

  但运行代码时出现了严重的错误。原因是没有在User()函数上调用new操作符。这种情况下,new操作符的缺失会导致函数是作为普通函数进行调用的,而不是构造器,而且没有实例化一个新对象。除此之外,构造器函数如果作为普通函数进行调用的话,会产生微妙的副作用,如污染当前作用域,从而导致更多意想不到的结果。以下代码不小心将变量引入到全局命名空间中:

function User(first, last){
this.name = first + " " + last;
}
var name = "Rukia";
var user = User("Ichigo", "Kurosaki");
assert(name == "Rukia",
"Name was set to Rukia.");

  如下代码能够判断函数是否是作为构造器进行调用:

function Test(){
return this instanceof arguments.callee;
}
assert(!Test(), "We didn't instantiate, so it returns false.");
assert(new Test(), "We did instantiate, returning true.");

  函数在作为构造器进行执行的时候,表达式:

this.instanceof arguments.callee

  它的结果是true,如果作为普通函数执行,则返回true.

在调用者上修复该问题

function User(first, last){
if (!(this instanceof arguments.callee)){
return new User(first, last);
}
this.name = first + " " + last;
}
var name = "Rukia";
var user = User("Ichigo", "Kurosaki");
assert(name == "Rukia", "Name was set to Rukia.");
assert(user instanceof User, "User instantiated");
assert(user.name == "Ichigo Kurosaki",
"User name correctly assigned");

  判断函数是否按照不正确的方式进行了调用,如果是,则再实例化User自身,将其作为函数的结果进行返回。这样做的结果是,无论我们函数调用的时候是否作为普通函数进行调用,最终都返回一个User实例。