JavaScript中的prototype原型学习指南_基础知识

原型是什么

Function 类型有一个属性 prototype,直接翻译过来就是原型。这个属性就是一个指针,指向一个对象,这个对象包含一些属性和方法,这些属性和方法会被当前函数生成的所有实例(对象)所共享。

这句话根据前面所说的,细细琢磨下来,就可以得到下面代码:

function Person(){
 ...
}
Person.prototype = {
 country : 'china',
 sayName : function(){
  ...
 }
}

先创建了一个 Function 类型的实例 person,然后 person 的方法 prototype 是一个对象,就声明指向了一个对象。这个对象里面的属性和方法,会被当前 person 函数生成的实例所共享。也就是说:

person1 = new Person();
person2 = new Person();

person1 和 person2 都是通过 Person 这个 Function 类型实例,再次生成的实例,它们俩都有共同的属性 country 和方法 sayName,因为它们都有某个指针(__proto__),直接指向 Person.prototype 所指向的对象。不过要注意 __proto__ 这个指针是不标准的,只有 Chrome 和 Firefox 等浏览器自己定义的,实际中,也不会用到这个属性,只是作为理解 prototype 来用:

关于原型等用法,后面会更具体的讲到。

创建对象的模式

下面,我们就来看下创建对象的方法和常用模式,以及它们之间的优缺点。

1.工厂模式

就像工厂一样,抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。通过使用函数代替部分重复工作,代码如下:

function createPerson(name, age, job){
 var o = new Object();
 o.name = name;
 o.age = age;
 o.job = job;
 o.sayName = function(){
  alert(this.name);
 };
 return o;
}
var person1 = createPerson("jiangshui","22","engineer");

这样就创建出来了一个人,工厂模式解决了多个相似对象重复创建问题,但是没有解决对象识别问题。只是单纯的创建了一个对象,而不管这个对象是从人类模版还是动物模版创建的,无法区分这个对象的类型。

2.构造函数模式

创建一个自定义的构造函数,从而定义自定义对象类型的属性和方法。

function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = jpb;
 this.sayName = function(){
  alert(this.name);
 };
};
var person1 = new Person(...);

3.构造函数模式与工厂模式区别:

  • 没有显式的创建对象。
  • 直接将属性和方法赋值 this 对象。
  • 没有 return 语句。

Person 是 Function 类型的对象,new 之后,会继续产生一个对象,但这个新产生的对象,由于在函数中传递进去参数,并赋值给了 this 指针,那么传递进去的内容,就变成了新产生对象的属性或方法。

构造函数默认习惯是首字母大写,上面代码执行经历了下面几个步骤:

  • 创建一个新对象
  • 将构造函数作用域赋值给新对象
  • 执行构造函数中的代码
  • 返回新对象

这样生成的实例中,都默认包含一个 constructor 属性指向构造函数,例如:

alert(person1.constructor == Person);

所以用构造函数模式,有类型的区分,可以将它的实例标识为一种特定的类型。

此外,构造函数就是普通的函数,因为要反馈得到新对象,所以用 new 来调用。如果不用的话,直接执行就跟普通函数一样,例如上面,执行 Person.sayName() 会弹出 window.name,因为函数在 window 下面执行,所以 this 指向 window。

构造函数模式也是有缺陷的,构造函数模式里面的方法,在每个实例上都重新创建了一遍,因此不同实例上的同名函数是不相等的。例如:

person1.sayName == person2.sayName; //false

也就是说,由构造函数生成的每个对象实例,属性和方法都是独有的,都是复制了一遍。属性独有是必须的,因为这正是对象之间不同的地方,但是很多方法功能和代码都是一样的,重复复制多次,显然就会浪费资源。

所以我们可以把函数放在外面,然后在构造函数里面,用指针指向这个函数,那么生成的实例中,方法存储的就是一个指向某函数的指针,也就共用一个函数了:

function Person(name, age){
 this.name = name;
 this.age = age;
 this.sayName = sayName;
}
function sayName(){
 alert(this.name);
}

但是这样,这个函数就变成了全局函数,而且与 Person 构造函数关联性不强,没有封装性可言。

下面有请原型模式登场。

原型模式

前面已经介绍了一部分关于原型的基础知识。简单的说,就是每个函数都有一个 prototype 属性,指向一个对象(原型对象),这个对象里面可以放一些属性或者方法。然后这个函数生成的实例,会有一个不规范的属性(__proto__)指向原型。

由此来看,你应该可以理解:prototype 产生的属性和方法是所有实例共享的。

这样正好解决了上面构造函数模式中,实例中函数的共用问题。例如下面代码:

function Person(){
 ....
}
Person.prototype.name = "jiangshui";
Person.prototype.sayName = function(){
 alert(this.name);
};

var person1 = new Person();
person1.sayName(); //jiangshui

或者

Person.prototype = {
 constructor : Person,
 name : "jiangshui",
 sayName : function(){
  alert(this.name);
 }
};

第二种方法覆盖了整个 prototype 对象,所以需要手动指定 constructor 属性,指向构造函数否则会指向 Object。

梳理一下它们的关系:

  • Person.prototype -》 原型对象,可以定义一些属性或者参数,被所有实例共用。
  • Person.prototype.constructor == Person —》 原型对象有个默认的属性 constructor 指向该原型对象所属的构造函数(注意另一种写法会覆盖掉这个属性,需要重新指定)。
  • person1 = new Person() -》构造函数生成实例,实例包含了构造函数的内容和原型对象的内容。
  • person1.__proto__ -》指向创建这个实例的原型对象(不规范,不要用)。

使用 isPrototypeOf() 可以确定对象之间的关系。例如:

Person.prototype.isPrototypeOf(person1);

当代码读取某个对象的某个属性,会执行搜索。先从当前对象开始,如果没有,则搜索指针指向的原型对象,而不会搜索构造函数。对象实例可以访问但是不能重写原型对象的值。如果实例中设置了与原型对象同名的属性,则搜索过程,在实例中结束而不会访问原型对象,所以达到覆盖的目的。因此即使这个属性设置为 null,也表示在实例中已经存在该属性,而不会取消掉这个属性,从而可以访问原型对应属性。

所以需要使用 delete 操作符,完全删除实例属性,从而可以重新访问原型。

原型是动态的,对原型对象所做的任何修改,都能立即从实例上反映出来。原因是实例与原型之间的松散链接关系,每次调用实例的属性方法,都会进行一次查询,如果原型变了,查询结果也就变了。

了解原型之后,我们也可以对原生对象添加新方法或属性。Object、Array、String 等原生引用类型,与上面构造函数类似,我们可以用 prototype 扩充它们的方法。例如:

String.prototype.startsWith = function(text){
 return this.indexOf(text) == 0;
};
var msg = "Hello World";
msg.startsWith("Hello");

这段代码为 String 这个原生引用类型,增加了一个 startsWith 方法,功能就是传递进去一个参数,看看要测试的字符串是否以参数开始。由于原型的动态性,所以只要执行一下,所有字符串类型的变量全都获得了这个方法。

但是不推荐使用这个方法,如果用的太多,代码太多,会导致维护困难、代码混乱等情况。一般情况下,会先继承某个原生引用类型,然后再在新自定义的类型上创建。关于继承,后面会再总结。

原型模式也不是万能的,原型中的所有属性和方法是被所有实例共享的,所以对于函数之类非常合适,而对于包含引用类型的属性来说,就会产生一些冲突。例如:

function Person(){}
Person.prototype = {
 constructor : Person,
 friends : ["greg","jack"]
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("tom");
console.log(person2.friends);

你会在 console 中看到,person2 的 friends 多了一个 tom,这并不是我想要的,但是对 person1 定义他的朋友时,的确影响到了实例 person2。

所以我们要结合原型模式和构造函数模式来使用。

组合使用构造函数模式和原型模式

这就是最常用的模式,构造函数用来定义实例属性,通过传递参数实现自定义;原型用来定义方法或者需要所有实例共享的属性。这样,既实现了自定义,又保证了共用,还避免了问题。

function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.friends = ["greg","jack"];
}
Person.prototype = {
 constructor : Person,
 sayName : function(){
  alert(this.name);
 }
};
var jiangshui = new Person("jiangshui","22","engineer");

实际应用示例

OK,到了这里,你可能会看懂原型是啥,以及如何创建对象,可是,这些又有什么用?确实,我之前的工作,一直也就是用 jQuery 写一些代码就可以了,根本用不到封装然后生成对象实现功能等。那这些究竟有什么用?

这种开发方式主要用于模块化和组建化的开发。比如你常用的弹窗功能,你当然可以把弹窗有关代码,每次都粘贴复制,然后修改一下就可以用在项目里面了。更好的选择是把你的弹窗功能代码,抽象封装成这样的一个组件,这样当你需要用弹窗的时候,只需要传递参数生成一个弹窗实例,就可以调用了。

原型对象和原型链

在Javascript中,万物皆对象,但对象也有区别,大致可以分为两类,即:普通对象(Object)和函数对象(Function)。

一般而言,通过new Function产生的对象是函数对象,其他对象都是普通对象。

举例说明:

function f1(){
  //todo
}
var f2 = function(){
  //todo
};
var f3 = new Function('x','console.log(x)');

var o1 = {};
var o2 = new Object();
var o3 = new f1();

console.log(
  typeof f1,//function
  typeof f2,//function
  typeof f3,//function
  typeof o1,//object
  typeof o2,//object
  typeof o3 //object
);
>> function function function object object object

f1属于函数的声明,最常见的函数定义方式,f2实际上是一个匿名函数,把这个匿名函数赋值给了f2,属于函数表达式,f3不常见,但也是一种函数对象。

Function是JS自带的对象,f1,f2在创建的时候,JS会自动通过new Function()的方式来构建这些对象,因此,这三个对象都是通过new Function()创建的。

在Javascript中创建对象有两种方式:对象字面量和使用new表达式,o1和o2的创建恰好对应了这两种方式,重点讲一下o3, 如果用Java和C#的思路来理解的话,o3是f1的实例对象,o3和f1是同一类型,至少我以前这么认为,其实不然…

那么怎么理解呢? 很简单,看o3是不是通过new Function产生的, 显然不是,既然不是函数对象,那就是普通对象 。

通过对函数对象和普通对象的简单理解之后,我们再来了解一下Javascript中的原型和原型链:

在JS中,每当创建一个函数对象f1 时,该对象中都会内置一些属性,其中包括prototype和__proto__,  prototype即原型对象,它记录着f1的一些属性和方法。

需要注意的是,prototype 对f1是不可见的,也就是说,f1不会查找prototype中的属性和方法。

function f(){}
f.prototype.foo = "abc";
console.log(f.foo); //undefined

那么,prototype有什么用呢? 其实prototype的主要作用就是继承。 通俗一点讲,prototype中定义的属性和方法都是留给自己的“后代”用的,因此,子类完全可以访问prototype中的属性和方法。

想要知道f1是如何把prototype留给“后代”,我们需要了解一下JS中的原型链,此时,JS中的 __proto__ 入场了,这哥们长的很奇特,隐藏的也很深,以致于你经常见不到它,但它在普通对象和函数对象中都存在, 它的作用就是保存父类的prototype对象,JS在通过new 表达式创建一个对象的时候,通常会把父类的prototype赋值给新对象的__proto__属性,这样,就形成了一代代传承…

function f(){}
f.prototype.foo = "abc";
var obj = new f();
console.log(obj.foo); //abc

现在我们知道,obj中__proto__保存的是f的prototype, 那么f的prototype中的__proto__中保存的是什么呢? 看下图:

如图所示,f.prototype的__proto__中保存的是Object.prototype,Object.prototype对象中也有__proto__,而从输出结果看,Object.prototype.__proto__ 是null,表示obj对象原型链的终结。如下图所示:

obj对象拥有这样一个原型链以后,当obj.foo执行时,obj会先查找自身是否有该属性,但不会查找自己的prototype,当找不到foo时,obj就沿着原型链依次去查找…

在上面的例子中,我们在f的prototype上定义了foo属性,这时obj就会在原型链上找到这个属性并执行。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索javascript
, js
, 继承
, prototype
原型
,以便于您获取更多的相关知识。

时间: 2024-12-24 07:00:25

JavaScript中的prototype原型学习指南_基础知识的相关文章

JavaScript中的Object对象学习教程_基础知识

参数:(1)obj 必需.Object 对象分配到的变量名称.  (2)值 可选.任一 JavaScript 基元数据类型(数字.布尔值或字符串).  如果值是一个对象,则返回的对象是未修改的.  如果值是 null."未定义"或"未提供",则创建无内容的对象.  Object对象的方法 Object作为构造函数使用时,可以接受一个参数.如果该参数是一个对象,则直接返回这个对象:如果是一个原始类型的值,则返回该值对应的包装对象.利用这一点,可以写一个判断变量是否为对

JavaScript中的prototype.bind()方法介绍_基础知识

以前,你可能会直接设置self=this或者that=this等等,这样做当然也能起作用,但是使用Function.prototype.bind()会更好,看上去也更专业.下面举个简单的例子: 复制代码 代码如下: var myObj = {    specialFunction: function () {    },    anotherSpecialFunction: function () {    },    getAsyncData: function (cb) {        c

再JavaScript的jQuery库中编写动画效果的指南_基础知识

jquery中常用的动画的方法就是hide()与show(). $(element).hide()这段代码可以与这相等element.css("display","none")  在hide(time)与show(time)中填入事件,可以慢慢消失跟显现.可以修改元素的多个样式,高度,宽度,不透明度. 另一组方法fadeIn()与fadeOut()这个与hide跟show不同的是,当使用hide或者show的时候会改变网页的高度,而fadeIn与fadeOut则不会

JavaScript的代码编写格式规范指南_基础知识

对于熟悉 C/C++ 或 Java 语言的工程师来说,JavaScript 显得灵活,简单易懂,对代码的格式的要求也相对松散.很容易学习,并运用到自己的代码中.也正因为这样,JavaScript 的编码规范也往往被轻视,开发过程中修修补补,最终也就演变成为后续维护人员的恶梦.软件存在的长期价值直接与编码的质量成比例.编码规范能帮助我们降低编程中不必要的麻烦.而 JavaScript 代码是直接发送给客户浏览器的,直接与客户见面,编码的质量更应该受到关注. 本文浅谈 JavaScript 编程中关

简单理解JavaScript中的封装与继承特性_基础知识

JavaScript中的封装封装简单地说就是让外界只能访问对象的共有变量和函数,隐藏细节和数据. js中有三种方法创建对象,分别为门户大开型.用命名规范区分私有变量.闭包创建真正的私有变量三种. 1.门户大开型,是实现对象的最基础的方法,所有方法与变量都是共有的外界可以访问. var Book = function(name){ if(this.check(name)){ console.log("error"); throw new Error("name null&quo

JavaScript中数组继承的简单示例_基础知识

在写一些库时经常会用到树结构的数据,而且一些树形结构的数据对从根到叶的路径获取需求非常高.比如一个站点的整个路由表就是一棵这样的树,它的「路径」实际上就是 URL 中的 path 部分.所以我好几次都用了丧心病狂的数组继承去实现,下面给大家分享下. 在 JavaScript 中,数组也属于 Object 的一种,它也可以继承.任何两个对象本身就可以有继承关系,数组也不例外.于是我们让一个树的任何一个节点都是数组,它只维护自己下标最大的那个元素的值.其它元素的值通过原型继承从祖先节点继承而来.这样

JavaScript中的Promise使用详解_基础知识

许多的语言,为了将异步模式处理得更像平常的顺序,都包含一种有趣的方案库,它们被称之为promises,deferreds,或者futures.JavaScript的promises ,可以促进关注点分离,以代替紧密耦合的接口. 本文讲的是基于Promises/A 标准的JavaScript promises.[http://wiki.commonjs.org/wiki/Promises/A] Promise的用例:     执行规则     多个远程验证     超时处理     远程数据请求

详解JavaScript中undefined与null的区别_基础知识

有点奇怪的是,JavaScript语言居然有两个表示"无"的值:undefined和null.这是为什么? 一.相似性在JavaScript中,将一个变量赋值为undefined或null,老实说,几乎没区别. 复制代码 代码如下: var a = undefined;var a = null; 上面代码中,a变量分别被赋值为undefined和null,这两种写法几乎等价.undefined和null在if语句中,都会被自动转为false,相等运算符甚至直接报告两者相等. 复制代码

Javascript中的delete操作符详细介绍_基础知识

一.变量 说到javascript中的delete操作符,还是首先要搞清楚javascript中的变量和属性之间的关系. javascript中,变量和对象属性关系非常微妙,甚至可以很多时候会被等同起来,因为 javascript 在执行脚本之前会创建一个global对象,在浏览器中就是window对象,所有的全局变量都是这个global对象的属性,执行函数时也会创建一个activation对象,所有的局部变量都是这个activation对象的属性.这些可以大家可以去了解一下javascript