深入浅出JavaScript面向对象编程

0 写在前面的话

大多数的面向对象编程语言中,比如C++和Java,在使用他们完成任务之前,必须创建类(class)。但在JavaScript中并不需要或者说不强制使用类。
面向对象都有如下几种特性:

  1. 封装
    数据可以和数据操作的功能组织在一起
  2. 聚合
    一个对象可以引用另一个对象
  3. 继承
    一个新创建的对象和另一个对象拥有同样的特性,而无需显示复制功能
  4. 多态
    一个接口可以被多个对象实现

1 一切都是对象

“一切都是对象”这句话的重点在于如何去理解“对象”这个概念。
——当然,也不是所有的都是对象,值类型就不是对象。

首先咱们还是先看看javascript中一个常用的运算符——typeof。typeof应该算是咱们的老朋友,还有谁没用过它?

typeof函数输出的一共有几种类型,在此列出:
console.log(typeof x); // undefined 
console.log(typeof 10); // number
console.log(typeof 'abc'); // string
 console.log(typeof true); // boolean
console.log(typeof function () {}); //function 
console.log(typeof [1, 'a', true]); //object
console.log(typeof { a: 10, b: 20 }); //object 
console.log(typeof null); //object 
console.log(typeof new Number(10)); //object

以上代码列出了typeof输出的集中类型标识,其中上面的四种(undefined, number, string, boolean)属于简单的值类型,不是对象。剩下的几种情况——函数、数组、对象、null、new Number(10)都是对象。他们都是引用类型,也就是我们说的对象。

1.1 对象的创建

ES5里面有两种方法可以创建对象,或者说实例化对象

  1. new构造函数
  2. 对象字面量
    在ES6当中, 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
var birth = '2000/01/01';
var Person = {
  name: '张三',
  //等同于birth: birth
  birth,
  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }
};

1.2 函数和对象的关系

函数是一种对象,但是函数却不像数组一样——你可以说数组是对象的一种,因为数组就像是对象的一个子集一样。但是函数与对象之间,却不仅仅是一种包含和被包含的关系,函数和对象之间的关系比较复杂,甚至有一点鸡生蛋蛋生鸡的逻辑。

        function Fn() {
            this.name = '张三';
            this.year = 1988;
        }
        var fn1 = new Fn();

这个例子很简单,它能说明:对象可以通过函数来创建。对!也只能说明这一点。
但是我要说——对象都是通过函数创建的——有些人可能反驳:不对!因为:

var obj = { a: 10, b: 20 };

其实以上代码的本质是:

var obj = new Object();
        obj.a = 10;
        obj.b = 20;

所以,可以很负责任的说——对象都是通过函数来创建的。现在是不是糊涂了—— 对象是函数创建的,而函数却又是一种对象。天哪!函数和对象到底是什么关系啊?

2 原型对象

我们可以把原型对象看作是对象的基类。几乎所有的函数都有一个名为prototype的属性,该属性是一个原型对象用来创建新的对象实例。所有创建的对象实例共享该原型对象。

2.1 prototype属性

这个prototype的属性值是一个对象(属性的集合,再次强调!),默认的只有一个叫做constructor的属性,指向这个函数本身。


image.png

原型既然作为对象,属性的集合,不可能就只弄个constructor来玩玩,肯定可以自定义的增加许多属性。例如这位Object大哥,人家的prototype里面,就有好几个其他属性。


image.png

一个对象实例通过内部属性[[prototype]]追踪原型对象。该属性是一个指向该实例使用的原型对象的指针,当new一个新的对象时,构造函数的原型对象就会赋给该对象的prototype属性。
 function Fn() { }
        Fn.prototype.name = '张三';
        Fn.prototype.getYear = function () {
            return 1988;
        };

        var fn = new Fn();
        console.log(fn.name);
        console.log(fn.getYear());

Fn是一个函数,fn对象是从Fn函数new出来的,这样fn对象就可以调用Fn.prototype中的属性。

因为每个对象都有一个隐藏的属性——“proto”,这个属性引用了创建这个对象的函数的prototype。即:fn.proto === Fn.prototype

2.2 隐式原型 __proto__

每个函数function都有一个prototype,即原型。这里再加一句话——每个对象都有一个proto,可成为隐式原型。
该属性没有写入 ES6 的正文,而是写入了附录,原因是__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。
在实现上,__proto__调用的是Object.prototype.__proto__

image.png

那么上图中的“Object prototype”也是一个对象,它的proto指向哪里?

好问题!

在说明“Object prototype”之前,先说一下自定义函数的prototype。自定义函数的prototype本质上就是和 var obj = {} 是一样的,都是被Object创建,所以它的proto指向的就是Object.prototype。
但是Object.prototype确实一个特例——它的__proto__指向的是null,切记切记!

image.png

对象的proto指向的是创建它的函数的prototype,就会出现:Object.proto === Function.prototype。用一个图来表示。

image.png

上图中,很明显的标出了:自定义函数Foo.proto指向Function.prototype,Object.proto指向Function.prototype,唉,怎么还有一个……Function.proto指向Function.prototype?这不成了循环引用了?是的,它是一个环形结构。
其实稍微想一下就明白了。Function也是一个函数,函数是一种对象,也有__proto__属性。既然是函数,那么它一定是被Function创建。所以——Function是被自身创建的。所以它的__proto__指向了自身的Prototype。

最后一个问题:Function.prototype指向的对象,它的__proto__是不是也指向Object.prototype

答案是肯定的。因为Function.prototype指向的对象也是一个普通的被Object创建的对象,所以也遵循基本的规则。

2.3 instanceof

Instanceof运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为B。

Instanceof的判断队则是:沿着A的proto这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。

image.png

问题又出来了。Instanceof这样设计,到底有什么用?到底instanceof想表达什么呢?
重点就这样被这位老朋友给引出来了——继承——原型链。
即,instanceof表示的就是一种继承关系,或者原型链的结构

3 继承

继承是JavaScript面向对象编程中非常重要的概念,js中不支持借口继承,实现继承主要依靠原型链来实现。

3.1原型链

原型链是什么我们已经在上文中详细讲过了。原型链继承基本思想就是让一个原型对象指向另一个类型的实例。

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
  return this.subproperty
}
var instance = new SubType()
console.log(instance.getSuperValue()) // true

代码定义了两个类型SuperType和SubType,每个类型分别有一个属性和一个方法,SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。

实现的本质是重写原型对象,代之以一个新类型的实例,那么存在SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。
我们知道,在创建一个实例的时候,实例对象中会有一个内部指针指向创建它的原型,进行关联起来,在这里代码SubType.prototype = new SuperType(),也会在SubType.prototype创建一个内部指针,将SubType.prototype与SuperType关联起来。

所以instance指向SubType的原型,SubType的原型又指向SuperType的原型,继而在instance在调用getSuperValue()方法的时候,会顺着这条链一直往上找。

添加方法

在给SubType原型添加方法的时候,如果,父类上也有同样的名字,SubType将会覆盖这个方法,达到重新的目的。 但是这个方法依然存在于父类中。

注意
记住不能以字面量的形式添加,因为,上面说过通过实例继承本质上就是重写,再使用字面量形式,又是一次重写了,但这次重写没有跟父类有任何关联,所以就会导致原型链截断。

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype = {
  getSubValue:function () {
   return this.subproperty
  }
}
var instance = new SubType()
console.log(instance.getSuperValue())  // error

缺点
单纯的使用原型链继承,主要问题来自包含引用类型值的原型。在构造函数定义一个属性后,通过原型继承这个属性就会出现在继承的prototype中,继承后的所有实例都会共享这个属性


3.2借用构造函数

此方法为了解决原型中包含引用类型值所带来的问题。

这种方法的思想就是在子类构造函数的内部调用父类构造函数,可以借助apply()和call()方法来改变对象的执行上下文

function SuperType() {
  this.colors = ['red', 'blue', 'green']
}
function SubType() {
  // 继承SuperType
  SuperType.call(this)
}
var instance1 = new SubType()
var instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors)  // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green"]

传递参数

借助构造函数还有一个优势就是可以传递参数

function SuperType(name) {
  this.name = name
}
function SubType() {
  // 继承SuperType
  SuperType.call(this, 'Jiang')
 
  this.job = 'student'
}
var instance = new SubType()
console.log(instance.name)  // Jiang
console.log(instance.job)   // student

如果仅仅借助构造函数,方法都在构造函数中定义,因此函数无法达到复用

3.3 组合继承(原型链+构造函数)

组合继承是将原型链继承和构造函数结合起来,从而发挥二者之长的一种模式。

思路就是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 继承属性
  SuperType.call(this, name)

  this.job = job
}
// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SuperType
SubType.prototype.sayJob = function() {
  console.log(this.job)
}
var instance1 = new SubType('Jiang', 'student')
instance1.colors.push('black')
console.log(instance1.colors) //["red", "blue", "green", "black"]
instance1.sayName() // 'Jiang'
instance1.sayJob()  // 'student'
var instance2 = new SubType('J', 'doctor')
console.log(instance2.colors) // //["red", "blue", "green"]
instance2.sayName()  // 'J'
instance2.sayJob()  // 'doctor'

这种模式避免了原型链和构造函数继承的缺陷,融合了他们的优点,是最常用的一种继承模式。

3.4 原型式继承

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

在object函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

本质上来说,object对传入其中的对象执行了一次浅复制。

var person = {
  name: 'Jiang',
  friends: ['Shelby', 'Court']
}
var anotherPerson = object(person)
console.log(anotherPerson.friends)  // ['Shelby', 'Court']

Object.create()方法

ES5通过Object.create()方法规范了原型式继承,可以接受两个参数,一个是用作新对象原型的对象和一个可选的为新对象定义额外属性的对象,行为相同,基本用法和上面的object一样,除了object不能接受第二个参数以外。

Object.create(proto, [ propertiesObject ])

proto
一个对象,应该是新创建的对象的原型。
propertiesObject
可选。该参数对象是一组属性与值,该对象的属性名称将是新创建的对象的属性名称,值是属性描述符(这些属性描述符的结构与Object.defineProperties()
的第二个参数一样)。注意:该参数对象不能是 undefined
,另外只有该对象中自身拥有的可枚举的属性才有效,也就是说该对象的原型链上属性是无效的。

3.5寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数。

function createAnother(o) {
  var clone = Object.create(o) // 创建一个新对象
  clone.sayHi = function() { // 添加方法
    console.log('hi')
  }
  return clone  // 返回这个对象
}
var person = {
  name: 'Jiang'
}
var anotherPeson = createAnother(person)
anotherPeson.sayHi()

基于person返回了一个新对象anotherPeson,新对象不仅拥有了person的属性和方法,还有自己的sayHi方法。

在主要考虑对象而不是自定义类型和构造函数的情况下,这是一个有用的模式。

3.7寄生组合式继承

在前面说的组合模式(原型链+构造函数)中,继承的时候需要调用两次父类构造函数。

父类

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

第一次在子类构造函数中

function SubType(name, job) {
  // 继承属性
  SuperType.call(this, name)

  this.job = job
}

第二次将子类的原型指向父类的实例

// 继承方法
SubType.prototype = new SuperType()

当使用var instance = new SubType()的时候,会产生两组name和color属性,一组在SubType实例上,一组在SubType原型上,只不过实例上的屏蔽了原型上的。

使用寄生式组合模式,可以规避这个问题。
这种模式通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

基本思路:不必为了指定子类型的原型而调用父类的构造函数,我们需要的无非就是父类原型的一个副本。

本质上就是使用寄生式继承来继承父类的原型,在将结果指定给子类型的原型。

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype)
  prototype.constructor = subType
  subType.prototype = prototype
}

该函数实现了寄生组合继承的最简单形式。

这个函数接受两个参数,一个子类,一个父类。

第一步创建父类原型的副本,第二步将创建的副本添加constructor属性,第三部将子类的原型指向这个副本。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 继承属性
  SuperType.call(this, name)
 
  this.job = job
}
// 继承
inheritPrototype(SubType, SuperType)
var instance = new SubType('Jiang', 'student')
instance.sayName()

补充:直接使用Object.create来实现,其实就是将上面封装的函数拆开,这样演示可以更容易理解。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 继承属性
  SuperType.call(this, name)
 
  this.job = job
}
// 继承
SubType.prototype = Object.create(SuperType.prototype)
// 修复constructor
SubType.prototype.constructor = SubType
var instance = new SubType('Jiang', 'student')
instance.sayName()

ES6新增了一个方法,Object.setPrototypeOf,可以直接创建关联,而且不用手动添加constructor属性。

3.8 ES6实现继承

class, extends, super

这三个特性涉及了ES5中最令人头疼的的几个部分:原型、构造函数,继承...你还在为它们复杂难懂的语法而烦恼吗?你还在为指针到底指向哪里而纠结万分吗?

有了ES6我们不再烦恼!
ES6提供了更接近传统语言的写法,引入了Class(类)这个概念。新的class写法让对象原型的写法更加清晰、更像面向对象编程的语法,也更加通俗易懂。

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        console.log(this.type + ' says ' + say)
    }
}

let animal = new Animal()
animal.says('hello') //animal says hello

class Cat extends Animal {
    constructor(){
        super()
        this.type = 'cat'
    }
}

let cat = new Cat()
cat.says('hello') //cat says hello

上面代码首先用class定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。简单地说,constructor内定义的方法和属性是实例对象自己的,而constructor外定义的方法和属性则是所有实例对象可以共享的

Class之间可以通过extends关键字实现继承*,这比ES5的通过修改原型链实现继承,要清晰和方便很多。上面定义了一个Cat类,该类通过extends
关键字,继承了Animal类的所有属性和方法。

super关键字,它指代父类的实例(即父类的this对象)。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

ES6的继承机制,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

P.S 如果你写react的话,就会发现以上三个东西在最新版React中出现得很多。创建的每个component都是一个继承React.Component
的类。详见react文档

4 执行上下文

在全局环境下的代码段中,执行上下文环境中有如何数据:

  • 变量、函数表达式——变量声明,默认赋值为undefined;
  • this——赋值;
  • 函数声明——赋值

如果在函数中,除了以上数据之外,还会有其他数据。在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

另外一点不同在于,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。
给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。
讲完了上下文环境,又来了新的问题——在执行js代码时,会有数不清的函数调用次数,会产生许多个上下文环境。这么多上下文环境该如何管理,以及如何销毁而释放内存呢?下面将通过“执行上下文栈”来解释这个问题。

完成一段js代码

  • 引擎
  • 编译器
  • 作用域
var a = 1;

分为两步进行

  1. var a编译器询问当前作用域是否存在a,如果是,忽略该声明;如果是,编译器会在当前作用域创建一个新变量,并命名为a
  2. 编译器为引擎生成运行时所需要的代码。引擎运行时会询问当前作用域,是否存在一个a变量,如果存在,就会继续使用,如果不存在就会继续寻找,如果找到就为它赋值

有关编辑器,编辑器在第二步生成代码,引擎执行前,会通过查找a观察他是否被声明,会通过作用域查找他是否申明过,查询类型有两种,一种为LHS,一种为RHS.

LHS

赋值操作的左侧进行查询,查找的目的是为了给变量赋值。

RHS

赋值操作的右侧进行查询,查询的目的是为了查询变量的值

作用域的嵌套

lhs和rhs查询都会在当前作用域开始查找,如果有需要,就回向上层作用域继续查找,直到顶层。

异常

RHS失败会抛出一个regerenceerror错误异常,不成功的LHS会创建一个全局变量(非严格模式下)。

4.1 词法作用域

欺骗词法

  • eval()
  • with 用作重复引用同一个对象中
    多个属性
with (obj){
    a= 3;
    b= 4;
    }

区别:eval()是修改了所处的词法作用域,而with()是创造了一个新的词法作用域:本质是通过一个对象的引用当做作用域处理,将对象的属性当做作用域中的标识符处理,创造了新的词法作用域
缺点:无法在编译时对作用域查找进行优化

(settimerout() ;setinterval();new function 等函数都可以接受字符串为参数,解释为动态执行的代码)

4.2 执行上下文

执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。


image.png

上代码

var a = 10,        //全局上下文
              fn ,
              bar = function(x){
                     var b= 5;
                     fn(x+ b);    //执行fn,进入fn的上下文
              };

fn = function(y) {
        var c=5;
        console.log( y +c);
}

bar(10);    //bar的上下文

在执行代码之前,首先将创建全局上下文环境。



然后是代码执行。代码执行到第12行之前,上下文环境中的变量都在执行过程中被赋值。



执行到第13行,调用bar函数。
跳转到bar函数内部,执行函数体语句之前,会创建一个新的执行上下文环境。

并将这个执行上下文环境压栈,设置为活动状态。



执行到第5行,又调用了fn函数。进入fn函数,在执行函数体语句之前,会创建fn函数的执行上下文环境,并压栈,设置为活动状态。

待第5行执行完毕,即fn函数执行完毕后,此次调用fn所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)。



同理,待第13行执行完毕,即bar函数执行完毕后,调用bar函数所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)。


4.3函数作用域

作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

所以,如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。

切记是要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记——其实这就是所谓的“静态作用域”。

4.4 闭包

使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
闭包有三个特性:
1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会被垃圾回收机制回收

于“闭包”这个词的概念的文字描述,确实不好解释,我看过很多遍,但是现在还是记不住。
但是你只需要知道应用的两种情况即可——函数作为返回值,函数作为参数传递。

  1. 函数作为返回值
  2. 函数作为参数被传递
function fn(){
    var max = 10;
    return function(x){
        if(x >max){
            console.log(x)
                }
    }
}
var f1 =fn();
f1(15)

第一步,代码执行前生成全局上下文环境,并在执行时对其中的变量进行赋值。此时全局上下文环境是活动状态。


第二步,执行第17行代码时,调用fn(),产生fn()执行上下文环境,压栈,并设置为活动状态。


第三步,执行完第17行,fn()调用完成。按理说应该销毁掉fn()的执行上下文环境,但是这里不能这么做。注意,重点来了:因为执行fn()时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量max要引用fn作用域下的fn()上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。
因此,这里的fn()上下文环境不能被销毁,还依然存在与执行上下文栈中。
——即,执行到第18行时,全局上下文环境将变为活动状态,但是fn()上下文环境依然会在执行上下文栈中。另外,执行完第18行,全局上下文环境中的max被赋值为100。如下图:


第四步,执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。



执行bar(15)时,max是自由变量,需要向创建bar函数的作用域中查找,找到了max的值为10。这个过程在作用域链一节已经讲过。
这里的重点就在于,创建bar函数是在执行fn()时创建的。fn()早就执行结束了,但是fn()执行上下文环境还存在与栈中,因此bar(15)时,max可以查找到。如果fn()上下文环境销毁了,那么max就找不到了。
使用闭包会增加内容开销,现在很明显了吧!

第五步,执行完20行就是上下文环境的销毁过程,这里就不再赘述了。

5 this

this的取值分四个情况
敲黑板:在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。

  1. 构造函数
    所谓构造函数就是用来new对象的函数。其实严格来说,所有的函数都可以new一个对象,但是有些函数的定义是为了new一个对象,而有些函数则不是。另外注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function等。
function Foo(){
    this.name="rxy";
    this.year="1996";
    console.log(this)     //Foo {name:"rxy",year:"1996"}
}
var fi =new Foo();   //实例化

以上代码中,如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象。
注意,以上仅限new Foo()的情况,即Foo函数作为构造函数的情况。如果直接调用Foo函数,而不是new Foo(),情况就大不一样了。

function Foo(){
    this.name="rxy";
    this.year="1996";
    console.log(this)     //Window
}
Foo() //这种情况下this是window
  1. 函数作为对象的一个属性
    如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象。
var obj = {
    x: 10,
    fn:function(){
        console.log(this)   //object {x:10,fn:function}
    }    
}
obj.fn

以上代码中,fn不仅作为一个对象的一个属性,而且的确是作为对象的一个属性被调用。结果this就是obj对象。

注意,如果fn函数不作为obj的一个属性被调用,会是什么结果呢?

var obj = {
    x: 10,
    fn:function(){
        console.log(this)   //object {x:10,fn:function}
    }    
}
var fn1= obj.fn
fn1()

如上代码,如果fn函数被赋值到了另一个变量中,并没有作为obj的一个属性被调用,那么this的值就是window,this.x为undefined。

  1. 函数用call或者apply或者bind
    当一个函数被call或者apply或者bind调用时。this的值就传入对象的值。

  2. 全局 & 调用普通函数
    在全局环境下,this永远是window,这个应该没有非议。

console.log(this === global);  //true

普通函数在调用时,其中的this也都是window。

var obj={
    x:10;
    fn:function(){
            function() f(){
                console.log(this);   // Window{...}
            }
    }
}

虽然f在obj内部定义的,但他任然是一个普通函数 this任然指向window
以上代码是从jQuery中摘除来的部分代码。jQuery.extend和jQuery.fn.extend都指向了同一个函数,但是当执行时,函数中的this是不一样的。

执行jQuery.extend(…)时,this指向jQuery;执行jQuery.fn.extend(…)时,this指向jQuery.fn。

这样就巧妙的将一段代码同时共享给两个功能使用,更加符合设计原则。

  1. 在构造函数的prototype中的this
function Fn(){
        this.name="rxy";
        this,age="20";
}
Fn.prototype.getName = function(){
        console.log(this.name)
}
var f1 = new Fn;
fi.getName  //rxy

如上代码,在Fn.prototype.getName函数中,this指向的是f1对象。因此可以通过this.name获取f1.name的值。
其实,不仅仅是构造函数的prototype,即便是在整个原型链中,this代表的也都是当前对象的值。

推荐阅读更多精彩内容

  • 1.对象是什么 对象就是若干属性的集合。 在JS中一切引用类型都是对象:数组是对象,函数是对象,对象还是对象。对象...
    liushaung阅读 604评论 0 2
  • 原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-c...
    jaysoul阅读 181评论 0 0
  • 1,javascript 基础知识 Array对象 Array对象属性 Arrray对象方法 Date对象 Dat...
    圆圆和方方阅读 228评论 0 1
  • 本文为深入理解javascript原型和闭包系列的摘要笔记 1.一切都是对象 对象:若干属性的集合。 java或者...
    JYOKETSU3阅读 47评论 0 0
  • 把莹莹送走,心里没事了,把架子打开,干活 这么复杂,还真有点畏难情绪,又想等着人家帮忙,依赖心理太严重。还是自己动...
    不二努力阅读 17评论 0 0