JavaScript 进阶营

JavaScript Tips - Vol.3 面向对象程序设计

2018-06-23  本文已影响1人  张羽辰

ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。” 相当于对象是一组没有特定顺序的值。

我们可以将 ECMAScript 的对象想象成散列表,无非就是一组 key-value,v 可以是数据或者函数。所以需要注意的是,JavaScript 并没有类的概念,不论是使用闭包或者原型,我们只是在模拟类的一个基本功能:模板

对象是一个引用类型创建的,该类型可以是 Object 也可以是其他自定义类型。

ECMAScript 有两种属性:数据与访问器

数据属性:

要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty()方法。

var people = {
    name: 'yuchen',
    age: 19,
    say: function () {
        console.log(this.name)
    }
};

people.say();

Object.defineProperty(people, 'name', {
    writable: false,
    value: 'yuchen'
});

people.name = 'lele';
people.say();

for (var t in people) {
    console.log(t);
    console.log(people[t]);
}

这个例子演示了一个对象中的属性,这个属性可以是一个 permitive value 也可以是 reference,那么当然也可以是一个函数。

访问器,为 getter setter 的提供者,只有 configurable、enumeration、get、set 四个属性。注意下面的 magic,如果

var boy = {};

Object.defineProperty(boy, 'name', {
    get: function () {
        return this._name; // this &  _name?
    },
    set: function (newVal) {
        this._name = newVal;
    }
});

上个例子中,我们使用了 this 与 _name。当你在思考 this 时,只需要抓住一点,谁调用了这个函数,谁就是 this。这一点在 Ruby 中有更好的描述,即 this 永远表示方法的 receiver。

所以在上面这个例子中,boy 这个对象,其实他有两个属性,name_name,对于 boy 对象来说,他们没有任何分别,都是属性,并有自己的值,我们使用 _name 的目的是为了更好的描述 getter 与 setter。

我经常在面试中问候选人一个很有意思的概念问题,function(函数) 与 method(方法) 的区别是什么?往往我们认为 method 是 function 概念上的子集,如果使用 OO 术语来说,method 是一个对象上的函数,对于 Ruby 大神来说,经常会用 receiver 这个词来描述,即 method 是一个有确定 receiver 的 function。

经常,在 OOP 中,我们倾向使用方法来定义这个类的行为,但是在 MVC 的一些范式中,是将数据与可以被操作的行为分割开的。定义行为的最佳实践并没有唯一解。

必先正名乎。

术语的正确使用可以帮你养成正确的交流习惯。

那么如何创建对象,最简单的方式也许就是工厂模式了,如果你对 Java 很熟悉,这一种方式对你就极为简单:

function createPerson(name, age) {
  return {
    name: name,
    age: age
  };
}

var yuchen = createPerson('yuchen', 19);

console.log(yuchen.name); // yuchen
console.log(typeof yuchen); // object

我们定义了一个返回新对象的函数,仅此而已,但是这样的一个对象非常简单,某些时候是满足我们的需求的。但是,typeof 关键字只能给我们 object 的值,我们没有解决这个 yuchen 是什么的问题。

function People(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function () {
        return this.name;
    }
}

var yuchen = new People("yuchen", 29);
var lele = new People("lele", 28);
console.log(yuchen.getName()); // yuchen
console.log(yuchen.getName === lele.getName); // false
console.log(typeof yuchen); // People

var o = {};
People.call(o, "another yuchen", 25); // this = o
console.log(o.getName()); // another yuchen

这个例子引入了一个新的概念,叫做构造函数。我们经常使用大写字母开始来表示这个函数是构造函数,并且使用 new 的关键字来创造一个对象。这样,我们能解决对象识别问题,typeof 关键字给了我们构造函数的名称,我们很开心。

但是,很可惜的,在 JavaScript 的世界中,并没有构造函数这个严谨的定义。构造函数是什么,构造函数就是一个函数,你当然可以用任何合法的字符定义一个函数。只是我们约定俗成的将可以用作对象创建,并且首字母大写的函数成为构造函数。那么构造函数的秘密是什么呢?就是 this 与 new。

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

someOne('yuchen'); // this.someOne('yuchen') or window.someOne('yuchen')

console.log(window.name); // yuchen
console.log(this); // window

这个例子说明了 this 与构造函数的关系,在浏览器的运行环境中,someOne function 是绑定在 window 对象中的,当调用 someOne 时,this 表示调用者,即 window,我们可以看到 name yuchen 被赋值给了 window 对象中的 name 属性中。

那么,new 关键字的意义是什么呢?可以简单的理解为 new 创建了一个空对象,并且将其作为 this 调用某个函数,再将其返回,大约如同下面的代码。

var o = {};
People.call(o, "another yuchen", 25); // this = o
console.log(o.getName()); // another yuchen

事实上的实现更复杂一些,我鼓励你去自己找到答案,因为我知道我的描述是不正确的或者不严谨的 :)

无论什么时候,只要你创建了一个函数,就会为其创建 prototype 属性,其指向函数的原型对象。原型对象的属性最初只包含 constructor,指原先对象的函数。例如 Person.prototype.constructor -> Person。当构造函数创建一个新实例后,该实例内部包含一个指针指向构造函数的原型。

虽然 ECMA-262 管这个指针叫做 Prototype,在 Firefox、Safari、Chrome 中每个对象都有 proto 用于表示原型,但这不是标准。原型最初只包含 constructor 属性,该属性也是共享的,所以可以通过对象实例访问。

function Person() {
}

Person.prototype.name = 'yuchen';
Person.prototype.sayName = function () {
    console.log(this.name);
};

var yuchen = new Person();

console.log(yuchen.hasOwnProperty("name")); // false

yuchen.sayName(); // yuchen

yuchen.name = 'zhang yuchen';
yuchen.sayName(); // zhang yuchen

console.log(yuchen.hasOwnProperty("name")); // true

这段代码描述了 JavaScript 很重要的特性:属性查找。规则很简单,现在对象上找这个属性,如果没有,就在原型上找。当我们第二次设置了 zhang yuchen 作为属性时,name 这个属性在 yuchen 这个对象上已经存在了,所以属性被找到。对于执行方法?方法是属性,定义在原型上,所以也被找到了,再加一对小括号,执行!

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

Person.prototype = {
    // constructor: Person,
    sayName: function () {
        console.log(this.name)
    }
};

var anotherYuchen = new Person('yuchen');
console.log(typeof anotherYuchen); // object ?!
console.log(anotherYuchen instanceof Object); // true
console.log(anotherYuchen instanceof Person); // true
console.log(anotherYuchen.constructor == Person); // false
console.log(anotherYuchen.constructor == Object); // true

prototype 上的 constructor 属性为我们解决了 typeof 问题,上面这个例子有点意外,为什么 typeof 返回的是 object ?

原型对象是指针,修改了就会切断最初原型的联系,一旦修改了原型,原有的指针还是指向旧的原型。参考下面这个例子:

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

Person.prototype.sayName = function () {
  console.log(this.name);
};

var yuchen = new Person('yuchen');
yuchen.sayName(); // yuchen

Person.prototype = {
  sayName: function() {
    console.log("I'm not: " + this.name);
  }
}
                
yuchen.sayName(); // yuchen

var cloneYuchen = new Person('yuchen');
cloneYuchen.sayName();  // I'm not: yuchen

当然,当你使用原型时,也要注意引用类型的情况,例如:

function Person() {
}

Person.prototype = {
    constructor: Person,
    name: 'yuchen',
    friends: ['lele', 'jd'],
    sayName: function () {
        console.log(this.name);
    }
};

var p1 = new Person();
var p2 = new Person();
p1.friends.push('duomi');
console.log(p2.friends); // lele jd duomi ???

这个就很好解释了,因为都是一个对象引用!那么如果你将 p1.friends = [] 然后再做 push 呢?

上一篇下一篇

猜你喜欢

热点阅读