Javascript

前言:关于javascript我总有些疑问,是不是一门面向对象的语言,总结了很久:Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,其实主要是因为它没有提供象抽象、继承、重载等有关面向对象语言的许多功能, 而是把其它语言所创建的复杂对象统一起来,从而形成一个非常强大的对象系统。 这种独特性称它为prototype-basedOO(基于原型的面向对象)。 (这是针对ES6以前的标准)

在这篇博客中我引用了阮一峰老师的教程:http://javascript.ruanyifeng.com/oop/basic.html 这个教程讲得十分详细,如果有系统学习的需要,建议跳过我的文章,直接看这里。

js面向对象编程

JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

  • 一、JavaScript 语言使用构造函数(constructor)作为对象的模板。

    var User = function () {

    this.age = 23;
    this.name = "wanghaoxin";

    };

这是一个构造函数。

构造函数的写法就是一个普通的函数,但是有自己的特征和用法。

  • 函数体内部使用了this关键字,代表了所要生成的对象实例。
  • 生成对象的时候,必需用new命令,调用Vehicle函数。

    var user = new User();//这里的等同于 var user = new User;
    user.age = 123;
    console.log(user.age);
    console.log(user.name);

执行这段代码结果就是 123 、 wanghaoxin,这里的user.age经过了赋值,所以是赋值后的值。

由于js并不是java一样的强类型语言,所以不能再编译前检查你的语法错误,这里有个问题。

> 一个很自然的问题是,如果忘了使用new命令,直接调用构造函数会发生什么事?

这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果。

  • 二、new命令的原理

使用new命令时,它后面的函数调用就不是正常的调用,而是依次执行下面的步骤。

    1. 创建一个空对象,作为将要返回的对象实例. 
    2. 将这个空对象的原型,指向构造函数的prototype属性.
    3. 将这个空对象赋值给函数内部的this关键字.
    4. 开始执行构造函数内部的代码.

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。

如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

  • 三、使用 Object.create() 创建实例对象

构造函数作为模板,可以生成实例对象。但是,有时只能拿到实例对象,而该对象根本就不是由构造函数生成的,这时可以使用Object.create()方法,直接以某个实例对象作为模板,生成一个新的实例对象。

    var person1 = {
      name: '张三',
      age: 38,
      greeting: function() {
        console.log('Hi! I\'m ' + this.name + '.');
      }
    };
    
    var person2 = Object.create(person1);
    
    person2.name // 张三
    person2.greeting() // Hi! I'm 张三.
  • 四、javascript使用prototype

JavaScript 的每个对象都继承另一个对象,后者称为“原型”(prototype)对象。null也可以充当原型,区别在于它没有自己的原型对象。JavaScript 继承机制的设计就是,原型的所有属性和方法,都能被子对象共享。

function Animal(name) {
  this.name = name;
}

Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'

这里的color属性被两个猫共享了。

cat1.color = 'black';

cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';

在这里cat1.color 被赋值为 black ,所以cat1将不再使用原来的原型属性.

prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。由于constructor属性定义在prototype对象上面,意味着可以被所有实例对象继承。

<font color=blue>构造函数就是普通的函数, 所以实际上所有函数都有prototype属性。</font>
  • 五、_proto_原型链

对象的属性和方法,有可能定义在自身,也有可能定义在它的原型对象。由于原型本身也是对象,又有自己的原型,所以形成了一条原型链(prototype chain)。比如,a对象是b对象的原型,b对象是c对象的原型,以此类推。

如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。那么,Object.prototype对象有没有它的原型呢?回答是有的,就是没有任何属性和方法的null对象,而null对象没有自己的原型。

“原型链”的作用是,读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。

如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。

  • Object.prototype.__proto__

根据语言标准,__proto__属性只有浏览器才需要部署,其他环境可以没有这个属性,而且前后的两根下划线,表示它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用Object.getPrototypeof()(读取)和Object.setPrototypeOf()(设置),进行原型对象的读写操作。

this及this的绑定

this的动态切换,固然为JavaScript创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this固定下来,避免出现意想不到的情况。JavaScript提供了call、apply、bind这三个方法,来切换/固定this的指向。

this的使用

这个关键字很好的体现了,如果不是面向对象,那为什么使用this呢,其实这个this还真有点不一样,在java中this就是指的本类,那么js中呢?

  • this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的。

所以这里需要注意的问题就是,到底是哪个对象最后调用了this,只有因为this本身指代的就是对象。下面的例子中this的指向变了,变成了谁(谁调用了), 一目了然。

function f() {
  return '姓名:'+ this.name;
}

var A = {
  name: '张三',
  describe: f
};

var B = {
  name: '李四',
  describe: f
};

A.describe() // "姓名:张三"
B.describe() // "姓名:李四"
  • 如果没有对象调用他,默认是用window对象调用的,这也讲得通,因为window是js的顶层对象,所有其他对象都是window的下属对象,JavaScript规定,浏览器环境的所有全局变量,都是window对象的属性。如果一个函数在全局环境中运行,那么this就是指顶层对象。

具体的例子看下面链接,比我写的好,
这里有详细的讲解http://www.cnblogs.com/pssp/p/5216085.html

call的使用

函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。call方法的参数,应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象。

var obj = {};

var f = function () {
  return this;
};

f() === this // true
f.call(obj) === obj // true

上面代码中,在全局环境运行函数f时,this指向全局环境;call方法可以改变this的指向,指定this指向对象obj,然后在对象obj的作用域中运行函数f。

var n = 123;
var obj = { n: 456 };

function a() {
  console.log(this.n);
}

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

上面代码中,a函数中的this关键字,如果指向全局对象,返回结果为123。如果使用call方法将this关键字指向obj对象,返回结果为456。可以看到,如果call方法没有参数,或者参数为null或undefined,则等同于指向全局对象。

function add(a, b) {
  return a + b;
}

add.call(this, 1, 2) // 3

上面代码中,call方法指定函数add内部的this绑定当前环境(对象),并且参数为1和2,因此函数add运行后得到3。

var obj = {};
obj.hasOwnProperty('toString') // false

// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

上面代码中,hasOwnProperty是obj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call方法可以解决这个问题,它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。

apply的使用

apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。

function f(x,y){
  console.log(x+y);
}

f.call(null,1,1) // 2
f.apply(null,[1,1]) // 2

上面的 f 函数本来接受两个参数,使用apply方法以后,就变成可以接受一个数组作为参数。

  • 绑定回调函数的对象

    var o = new Object();
    o.f = function () {
    console.log(this === o);
    }
    var f = function (){
    o.f.apply(o);
    // 或者 o.f.call(o);
    };
    $('#button').on('click', f);

点击按钮以后,控制台将会显示true。由于apply方法(或者call方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。更简洁的写法是采用下面介绍的bind方法。

bind的使用

bind方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。

var counter = {
  count: 0,
  inc: function () {
    this.count++;
  }
};

var func = counter.inc;
func();
counter.count // 0
count // NaN

上面代码中,函数func是在全局环境中运行的,这时inc内部的this指向顶层对象window,所以counter.count是不会变的,反而创建了一个全局变量count。因为window.count原来等于undefined,进行递增运算后undefined++就等于NaN。

为了解决这个问题,可以使用bind方法,将inc内部的this绑定到counter对象。

var func = counter.inc.bind(counter);
func();
counter.count // 1

Andy
54 声望11 粉丝

Remember,Hope is a good thing,maybe the best of things and no good thing ever dies!