JavaScript忍者秘籍——原型

概要:本篇博客主要介绍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是构造器的一个属性,所以每个对象都有一种方式可以找到自己的原型。

  在控制台输入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实例。

时间: 2024-10-12 11:41:39

JavaScript忍者秘籍——原型的相关文章

【JavaScript忍者秘籍】

【JavaScript】深入理解JavaScript之强大的原型和原型链

由于JavaScript是唯一一个被广泛使用的基于原型继承的语言,所以理解两种继承模式的差异是需要一定时间的,今天我们就来了解一下原型和原型链. AD: hasOwnProperty函数: hasOwnProperty是Object.prototype的一个方法,它可是个好东西,他能判断一个对象是否包含自定义属性而不是原型链上的属性,因为hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数. // 修改Object.prototype Object.p

JavaScript忍者阅读随笔(一):函数声明、调用

在JavaScript中,函数是作为一等成员而存在的,由此,非常有必要掌握JavaScript中函数的知识,最近几天阅读了JavaScript忍者的第三章和第四章前面的部分,做一个总结. JavaScript函数声明: JavaScript函数是使用 函数字面量 进行声明 从而创建函数的. 形如 function name(arg1,arg2)//函数的名称可选 { code; }; 函数的作用域: 在JavaScript中没有块级作用域,只有函数作用域.也就是说在JavaScript中,作用域

javascript精髓篇之原型链维护和继承.

一.两个原型 很多人都知道javascript是原型继承,每个构造函数都有一个prototype成员,通过它就可以把javascript的继承演义的美轮美奂了.其实啊,光靠这一个属性是无法完成javascript的继承.我们在代码中使用的prototype完成继承在这里就不多说了.大家可以查一下资料.另外一个看不见的prototype成员.每一个实例都有有一条指向原型的prototype属性,这个属性是无法被访问到的,当然也就无法被修改了,因为这是维护javascript继承的基础. 1 //构

javascript面向对象——构造函数和原型对象

一般地,javascript使用构造函数和原型对象来进行面向对象编程,它们的表现与其他面向对象编程语言中的类相似又不同.本文将详细介绍如何用构造函数和原型对象来创建对象 构造函数 构造函数是用new创建对象时调用的函数,与普通唯一的区别是构造函数名应该首字母大写 function Person(){ this.age = 30; } var person1 = new Person(); console.log(person1.age);//30 根据需要,构造函数可以接受参数 function

JavaScript学习--Item15 prototype原型和原型链详解

用过JavaScript的同学们肯定都对prototype如雷贯耳,但是这究竟是个什么东西却让初学者莫衷一是,只知道函数都会有一个prototype属性,可以为其添加函数供实例访问,其它的就不清楚了,最近看了一些 JavaScript高级程序设计,终于揭开了其神秘面纱. 每个函数都有一个prototype属性,这个属性是指向一个对象的引用,这个对象称为原型对象,原型对象包含函数实例共享的方法和属性,也就是说将函数用作构造函数调用(使用new操作符调用)的时候,新创建的对象会从原型对象上继承属性和

JavaScript高级特性之原型

JavaScript的原型 原型prototype属性只适用于函数对象(这里的函数对象是自己为了理解更好定义的,普通对象是没有原型属性的) 1.研究函数原型: <script type="text/javascript"> //原型是函数对象的一个属性(普通对象是没有原型属性的.). function Person(){ this.name="李卫康"; this.sayHi=function(){ alert("Hi"); } };

javascript基础集锦_Json——原型继承(十)

在传统的基于Class的语言如Java.C++中,继承的本质是扩展一个已有的Class,并生成新的Subclass. 由于这类语言严格区分类和实例,继承实际上是类型的扩展.但是,JavaScript由于采用原型继承,我们无法直接扩展一个Class,因为根本不存在Class这种类型. 但是办法还是有的.我们先回顾Student构造函数: function Student(props) { this.name = props.name || 'Unnamed'; } Student.prototyp

javascript --- 只继承于原型

正如上次所述,,出于效率考虑,我们应该尽可能的将一些可重用的属性和方法添加到原型中去. 如果养成了这个好习惯,我们仅仅依靠原型就能顺利的完成继承关系的构建了. 毕竟采用new her()方法将her的属性设为对象自身属性,这样的代码是不可重用的,我们可以利用下述方法提高一些效率.这节的内容上文已经提到,毕竟温故知新也是很重要的. 1. 不要单独为继承关系创建新对象. 2. 尽量减少运行时的方式方式搜索(例如toString()); 下面就是优化后的代码,修改的地方加粗显示(可能会与上文不太一样,