相信你已经知道了,Javascript函数也可以作为对象构造器。比如,为了模拟面向对象编程中的Class,可以用如下的代码

function Person(name){
this.name = name
}

注意:我不使用分号因为我是个异教徒!
不管怎么说,你现在有了一个function,你可以使用new操作符来创建一个Person

var bob = new Person('Bob')
// {name: 'Bob'}

为了确认bob确实是一个Person,可以这么做

bob instanceof Person
// true

你同样可以把Person作为一个普通函数调用——不使用new

Person('Bob')
// undefined

但是这里会返回undefined.同时,你在不经意间创建了一个全局变量name,这可不是你想要的。

name
// 'Bob'

嗯...这一点也不好,特别是如果你已经有一个名为name的全局变量,那么它将会被覆盖。这是因为你直接调用了一个函数(不适用new),this对象被设置为全局对象——在浏览器中,就是window对象。

window.name
// 'Bob'
this === window
// true

所以...如果你想写一个构造器函数,那么就用构造器的方式使用它(使用new),如果你想写一个普通函数,那么就以函数的方式使用它(直接调用),不要相互混淆。

注:一个较好的代码习惯就是,构造器函数首字母大写,普通函数首字母小写。如function Person(){}是一个构造器函数,function showMsg(){}是一个普通函数。

有些人也许会指出,可以使用一个小技巧避免污染全局变量。

function Person(name){
if (!(this instanceof Person))
return new Person(name)
this.name = name
}

这段代码做了三件事

  1. 检查this对象是否是Person的实例——如果使用new操作符的话就是。
  2. 如果它确实是Person的实例,执行原有的代码。
  3. 如果它不是Person的实例,使用new操作符创建一个Person的实例——这才是正确的使用姿势,然后返回它。

这就允许使用函数形式调用构造器函数,返回一个Person对象,不会污染全局命名空间。

Person('Bob')
// {name: 'Bob'}
name
// undefined

神奇的是使用new操作符同样可行

new Person('Bob')
// {name: 'Bob'}

为什么呢?这是因为当你使用new操作符创建一个对象时,如果你在构造函数里面主动返回一个对象,那么new表达式的值就是这个返回的对象;如果没有主动返回,那么构造函数会默认返回this。但是,你可能会想,我可不可以返回一个非Person对象呢?这就有点像欺诈了~

function Cat(name){
this.name = name
}
function Person(name){
return new Cat(name)
}
var bob = new Person('Bob')
bob instanceof Person
// false
bob instanceof Cat
// true

所以,我创建一个Person结果我得到了一个Cat?好吧,在Javascript中这确实可能发生。你甚至可以返回一个Array

function Person(name){
return [name]
}
new Person('Bob')
// ['Bob']

但是这有一个限制,如果你返回一个原始数据类型,返回值将不起作用。

function Person(name){
this.name = name
return 5
}
new Person('Bob')
// {name: 'Bob'}

Number,String,Boolean,都是原始数据类型。
如果你在构造器函数里面返回这些类型的值,那么它将会被忽略,构造器将按照正常情况,返回this对象。

注:原始数据类型还包含undefinednull。但如果你使用new操作符创建原始数据类型,它将会是一个对象

typeof (new String('hello')) === 'object' // true
typeof (String('hello')) === 'string' // true

方法

在最开始的时候我,我说过函数也可以作为构造器,事实上,它更像身兼三职。函数同样可以作为方法
如果你了解面向对象编程的话,你会知道方法是对象的行为——描述对象可以做什么。在Javascript中,方法就是链接到对象上的函数——你可以通过创建一个函数并把它赋值到对象上,来创建对象的方法。

function Person(name){
this.name = name
this.sayHi = function(){
return 'Hi, I am ' + this.name
}
}

Bob现在可以say Hi了!

var bob = new Person('Bob')
bob.sayHi()
// 'Hi, I am Bob'

事实上,我们可以脱离构造函数,创建对象的方法

var bob = {name: 'Bob'} // this is a Javascript object!
bob.sayHi = function(){
return 'Hi, I am ' + this.name
}

这同样可行。或者,如果你喜欢的话,把它写成一个更大的object

var bob = {
name: 'Bob',
sayHi: function(){
return 'Hi, I am ' + this.name
}
}

所以,我们为什么还需要构造函数呢?答案是继承。

原型和继承

好吧,我们谈谈继承。你肯定知道继承,对吧?比如在Java中,你可以让一个类继承另一个类,就可以自动得到所有父类的方法和变量了。

public class Mammal{
public void breathe(){
// do some breathing
}
}
public class Cat extends Mammal{
// now cat too can breathe!
}

那么,在Javascript中,我们可以做同样的事情,只是有些不同。首先,我们甚至没有类!取而代之的是prototype。下面就是与Java代码等价的Javascript代码。

function Mammal(){
}
Mammal.prototype.breathe = function(){
// do some breathing
}
function Cat(){
}
Cat.prototype = new Mammal()
Cat.prototype.constructor = Cat
// now cat too can breathe!

Javascript不同于传统的面向对象语言,它使用原型继承。简而言之,原型继承的工作原理如下:

  1. 一个对象有许多属性,包含普通属性和函数。
  2. 一个对象有一个特殊的父属性,它也被称为这个对象的原型,用__proto__表示。这个对象可以继承它父对象的所有属性。
  3. 一个对象可以通过在自身设置属性,重写父对象的的同名属性
  4. 构造器用于创建对象。每一个构造器都有一个相关联的prototype对象,它其实也是一个普通对象。
  5. 创建一个对象时,该对象的父对象(__proto__)被设置为创建它的构造器的prototype对象。

好的!现在你应该明白原型继承是怎么一回事了,接下来我们一行一行看Cat这个例子

首先,我们创建了一个构造器Mammal

function Mammal(){
}

这时候,Mammal已经有了一个prototype属性

Mammal.prototype
// {}

我们创建一个实例

var mammal = new Mammal()

现在,我们验证一下上面提到的第2条

mammal.__proto__ === Mammal.prototype
// true

接下来,我们在Mammalprototype属性上增加一个方法breathe

Mammal.prototype.breathe = function(){
// do some breathing
}

这时候,实例mammal就可以调用breathe了

mammal.breathe()

因为它从Mammal.prototype继承过来。往下

function Cat(){
}
Cat.prototype = new Mammal()

我们创建了一个Cat构造器,设置Cat.prototypeMammal的实例。为什么要这么做呢?

var garfield = new Cat()
garfield.breathe()

现在所有的cat实例都继承自Mammal,所以它也能够调用breathe方法,往下

Cat.prototype.constructor = Cat

确保cat确实是Cat的实例

garfield.__proto__ === Cat.prototype
// true
Cat.prototype.constructor === Cat
// true
garfield instanceof Cat
// true

每当你创建一个Cat的实例,你就会创建一个二级原型链,即garfieldCat.prototype的子对象,而Cat.prototypeMammal的实例,所以也是Mammal.prototype的子对象。

那么,Mammal.prototype的父对象是谁呢?没错,你也许猜到了,那就是Object.prototype。所以,实际上是三级原型链。

garfield -> Cat.prototype -> Mammal.prototype -> Object.prototype

你可以在garfield的父对象上增加属性,然后garfield就可以神奇的访问到这些属性,即使在garfield对象创建之后!

Cat.prototype.isCat = true
Mammal.prototype.isMammal = true
Object.prototype.isObject = true
garfield.isCat // true
garfield.isMammal // true
garfield.isObject // true

你也可以知道它是否有某个属性

'isMammal' in garfield
// true

并且你也可以区分自身的属性和继承而来的属性

garfield.name = 'Garfield'
garfield.hasOwnProperty('name')
// true
garfield.hasOwnProperty('breathe')
// false

在原型上创建方法

现在你应该理解了原型继承的原理,让我们回到第一个例子

function Person(name){
this.name = name
this.sayHi = function(){
return 'Hi, I am ' + this.name
}
}

直接在对象上定义方法是一种低效率的方式。一个更好的方法是在Person.prototype上定义方法。

function Person(name){
this.name = name
}
Person.prototype.sayHi = function(){
return 'Hi, I am ' + this.name
}

为什么这种方式更好?

在第一种方式中,每当我们创建一个person对象,一个新的sayHi方法就要被创建,而在第二种方式中,只有一个sayHi方法被创建了,并且在所有Person的实例中共享——这是因为Person.prototype是它们的父对象。所以,在prototype上创建方法会更加高效。

Apply & Call

正如你所见,函数凭借添加到对象上而成为了一个对象的方法,那么这个函数内的this指针应该始终指向这个对象,不是么?事实并不是这样。我们看看之前的例子。

function Person(name){
this.name = name
}
Person.prototype.sayHi = function(){
return 'Hi, I am ' + this.name
}

你创建两个Person对象,jackjill

var jack = new Person('Jack')
var jill = new Person('Jill')
jack.sayHi()
// 'Hi, I am Jack'
jill.sayHi()
// 'Hi, I am Jill'

在这里,sayHi方法不是添加在jack或者jill对象上的,而是添加在他们的原型对象上:Person.prototype。那么,sayHi方法如何知道jackjill的名字呢?

答案:this指针没有绑定到任何对象上,直到函数被调用时才进行绑定。

当你调用jack.sayHi()时,sayHithis指针就会绑定到jack上;当你调用jill.sayHi()是,它则会绑定到jill上。但是,绑定this对象不改变方法本身——它还是同样的一个函数!

你同样可以为一个方法指定所要绑定的this指针的对象。

function sing(){
return this.name + ' sings!'
}
sing.apply(jack)
// 'Jack sings!'

apply方法属于Function.prototype(没错,函数也是一个对象并且有prototypes和自身的属性!)。所以,你可以在任何函数中使用apply方法绑定this指针为指定的对象,即使这个函数没有添加到这个对象上。事实上,你甚至可以绑定this指针为不同的对象。

function Flower(name){
this.name = name
}
var tulip = new Flower('Tulip')
jack.sayHi.apply(tulip)
// 'Hi, I am Tulip'

你可能会说

等等,郁金香怎么会说话呢!

我可以回答你

任何人是任何事,任何事是任何人,颤抖吧人类@_@

只要这个对象有一个name属性,sayHi方法就会很乐意把它打印出。这就是鸭子类型准则

如果一个东西像鸭子一样嘎嘎叫,并且它走起来像鸭子一样,对我来说它就是鸭子!

那么回到apply函数:如果你想使用apply传递参数,你可以把它们构造成一个数组作为第二个参数。

function singTo(other){
return this.name + ' sings for ' + other.name
}
singTo.apply(jack, [jill])
// 'Jack sings for Jill'

Function.prototype也有call函数,它和apply函数非常相似,唯一的区别就是call函数依次把参数列在末尾传递,而apply函数接收一个数组作为第二个参数。

sing.call(jack, jill)
// 'Jack sings for Jill'

new方法

现在,有趣的事情来了。

当你想调用一个有若干个参数的函数时,apply方法十分的方便。比如,Math.max方法接受若干个number参数

Math.max(4, 1, 8, 9, 2)
// 9

这很好,但是不够抽象。我们可以使用apply获取到任意数组的最大值。

Math.max.apply(Math, myarray)

这有用多了!

既然apply这么有用,你可能会在很多地方想使用它,比起

Math.max.apply(Math, args)

你可能更想在构造器函数中使用

new Person.apply(Person, args)

遗憾的是,这不起作用。它会认为你把Person.apply整体当做了构造函数。那么这样呢?

(new Person).apply(Person, args)

这同样也不起作用,因为他会首先创建一个person对象,然后在尝试调用apply方法。

怎么办呢?StackOverflow上的这个回答是个好主意

我们可以在Function.prototype上创建一个new方法

Function.prototype.new = function(){
var args = arguments
var constructor = this
function Fake(){
constructor.apply(this, args)
}
Fake.prototype = constructor.prototype
return new Fake
}

这样,所有的构造器函数都有一个new方法

var bob = Person.new('Bob')

我们分析一下new方法的原理

首先

var args = arguments
var constructor = this
function Fake(){
constructor.apply(this, args)
}

我们创建了一个Fake构造器,在constructor上调用apply方法。在new方法的上下文中,this对象指的就是真实的构造器函数——我们把它保存在constructor变量中,同样的,我们也把new方法上下文的arguments保存在args变量中,以便在Fake构造器中使用。往下

Fake.prototype = constructor.prototype

我们设置Fake.prototype为原来的构造器的prototype。因为constructor指向的还是原始的构造函数,他的prototype属性还是原来的。所以通过Fake创建的对象还是原来的构造器函数的实例。最后

return new Fake

使用Fake构造器创建一个新对象并返回。

明白了么?第一次不明白没关系,多看几遍就能理解了!

总而言之,现在我们可以干一些很酷的事情了。

var children = [new Person('Ben'), new Person('Dan')]
var args = ['Bob'].concat(children)
var bob = Person.new.apply(Person, args)

很好!为了不写两遍Person,我们可以添加一个辅助方法

Function.prototype.applyNew = function(){
return this.new.apply(this, arguments)
}

现在你可以这样使用

var bob = Person.applyNew(args)

这就展示了Javascript是一门灵活的语言。即使它有些使用方法不是你想要的,你也可以模拟去做。

总结

这篇文章到这里就结束了,我们学习了

  1. Constructors构造器
  2. Methods and Prototypes方法和原型
  3. apply & call
  4. 实现一个new方法
文/文兴(简书作者)
原文链接:http://www.jianshu.com/p/322b90d489b8
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

Javascript构造函数和原型的更多相关文章

  1. Javascript 构造函数、原型对象、实例之间的关系

    # Javascript 构造函数.原型对象.实例之间的关系 # 创建对象的方式 # 1.new object() 缺点:创建多个对象困难 var hero = new Object(); // 空对 ...

  2. 深入javascript——构造函数和原型对象

    常用的几种对象创建模式 使用new关键字创建 最基础的对象创建方式,无非就是和其他多数语言一样说的一样:没对象,你new一个呀! var gf = new Object(); gf.name = &q ...

  3. 谈谈对Javascript构造函数和原型对象的理解

    对象,是javascript中非常重要的一个梗,是否能透彻的理解它直接关系到你对整个javascript体系的基础理解,说白了,javascript就是一群对象在搅..(哔!).   常用的几种对象创 ...

  4. 深入理解javascript构造函数和原型对象

    ---恢复内容开始--- 对象,是javascript中非常重要的一个梗,是否能透彻的理解它直接关系到你对整个javascript体系的基础理解,说白了,javascript就是一群对象在搅..(哔! ...

  5. javascript构造函数及原型对象

    /** @ javascript中没有类的概念,所以基在对象创建方面与面向对象语言有所不同* @ 对象创建的常用方法及各自的局限性* @ 使用Object或对象字面量创建对象* @ 工厂模式创建对象* ...

  6. javaScript构造函数、原型、面向对象编程

    js最重要也是最核心的东西就是对象了,入行这么长时间,一直对面向对象一知半解.网上有很多介绍对象对象的内容,这里也做了很多借鉴, 尤其是阮一峰老师的文章.我这里写的大多例子都是阮一峰老师文章的例子,但 ...

  7. [转]深入javascript——构造函数和原型对象

    对象,是javascript中非常重要的一个梗,是否能透彻的理解它直接关系到你对整个javascript体系的基础理解,说白了,javascript就是一群对象在搅..(哔!). 常用的几种对象创建模 ...

  8. [转]JavaScript构造函数及原型对象

    JavaScript中没有类的概念,所以其在对象创建方面与面向对象语言有所不同. JS中对象可以定义为”无序属性的集合”.其属性可以包含基本值,对象以及函数.对象实质上就是一组没有特定顺序的值,对象中 ...

  9. JavaScript 构造函数与原型链

    构造函数.原型链: function Person(name, age, job) { this.name = name; this.age = age; this.job = job; // thi ...

  10. javascript构造函数以及原型对象的理解

    以下是一个构造函数的例子 如果是实例方法,不同的实例化,它们引用的地址是不一样的,是唯一的. //定义一个构造函数 function People(name,age){ this.name=name; ...

随机推荐

  1. CF895E Eyes Closed (期望)

    题目链接 利用期望的线性性质: \(E(sum) = E(x_l) + E(x_{l+1})+ E(x_{l+2}) +.. E(x_r)\) 然后就考虑对于交换时两个区间元素的改动. 假设这两个区间 ...

  2. JS获取单选框checked的value方法

    ; var obj = document.getElementsByTagName("input"); document.getElementById('gender').oncl ...

  3. String中indexof函数的用法

    int indexOf(int ch) 返回指定字符在此字符串中第一次出现处的索引. int indexOf(int ch, int fromIndex) 从指定的索引开始搜索,返回在此字符串中第一次 ...

  4. Python模块(一)(常用模块)

    1. 简单了解模块 写的每一个py文件都是一个模块. 还有一些我们一直在使用的模块 buildins 内置模块. print, input random 主要是和随机相关的内容 random()    ...

  5. 初识Pyhon之准备环境

    安装成功python的运行环境之后,你可能要迫不及待大展身手了 如果你有一定的语言基础,那么基础这一块儿就可以简单的看看就可以了,但是你是一个编程语言的初学者.不着急,慢慢往下看 打开pycharm创 ...

  6. LeetCode(110) Balanced Binary Tree

    题目 Given a binary tree, determine if it is height-balanced. For this problem, a height-balanced bina ...

  7. 3,bool值之间的转换,和str的各个功能属性。

    bool值之间的转换 and 空字符串即为False   字符串内有内容即为True. a = 11 c = str(a) #int转换成str print(type(c)) a = ' b = in ...

  8. node.js中的http.request方法使用说明

    http.get(options, callback) 由于该方法属于http模块,使用前需要引入http模块(var http= require(“http”) ) 接收参数: option   数 ...

  9. vmware安装CentOS " Intel VT-x 处于禁用状态"

    我刚用虚拟机vmware 11安装CentOS 7 ,出现错误“此主机支持 Intel VT-x,但 Intel VT-x 处于禁用状态”的问题,如下图. Intel VT-x 即Virtualiza ...

  10. Working out (DP)

    Summer is coming! It's time for Iahub and Iahubina to work out, as they both want to look hot at the ...