JavaScript核心技术开发解密--第九章 面向对象

一、对象的定义

对象
在ECMAScript-262中,对象被定义为无序属性的集合,其属性可以包含基本值、对象或者函数。即是对象是由一系列无序的key-value对组成。其中value可以使基本数据类型,对象,数组,函数。下面就是一个简单的对象

var person = {
  // 属性为基本值
  name: 'Tom',
  age: 18,
  // 属性为函数
  getName: function () {
    return this.name;
  },
  // 属性为对象
  parent: {}
}

二、创建对象
本书中介绍了创建对象的两种方法:
1、通过关键字new来创建对象

var obj = new Object();

new一个关键字在创建实例的过程中分为三步:
①、创建一个obj对象
②、把对象的proto指向函数的prototype
③、再调用call方法,把F函数的对象替换成obj(call方法的作业:调用一个对象的一个方法,以另一个对象替换当前对象)。
object.create实在两个对象的prototype中间加一个中间类C,使子类不会污染父类。
2、 通过字面量的形式创建对象

var obj = {};
//创建属性和方法
var person = {};
person.name = 'Tom';
person.getName = function () { 
return this.name; }
// or 
var person = { 
name: 'Tom',
getName: function () { return this.name; } 
}
//访问属性和方法
var person = { 
  name: 'Tom', age: 20,
  getName: function () { return this.name; }
} // 访问name属性
person.name; 
// or
person['name']; //当我们访问的属性名是一个变量时,只能使用中括号的方式
// or 注意这里_name是一个变量 
var _name = 'name'; person[_name];

三、构造函数与原型
封装函数其实是封装一些公共的逻辑与功能,通过传入参数的形式达到自定义的效果。当面对具有共同特征的一类事物时,就可以结合构造函数与原型的方式将这类事物封装成对象。
举个栗子,我们将“人”这一类事物封装成一个对象,那么可以这样做:

// 构造函数,约定采用大写
var Person = function (name, age) { 
this.name = name; 
this.age = age; 
} 
// Person.prototype为Person的原型,这里在原型上添加了一个方法 Person.prototype.getName = function () { 
return this.name;
}

具体到某一个人专属的特点属性,通常放在构造函数当中,公共的方法与属性,通常会放在原型对象中

var p1 = new Person('Jake', 20);
var p2 = new Person('Tom', 22);
p1.getName(); // Jake
p2.getName(); // Tom

构造函数的this和原型方法中的this指向的都是当前实例,记下来实现一个New方法:

//将构造函数以参数的形式传入
function New(funA) {
   //声明一个中间对象,改对象为最终返回的实例
   var res =  {};
   if(funA.prototype !== null) {
      //实例的原型指向构造函数的原型
   res.__proto__ == funA.prototype;
   }  
//ret为构造函数的执行结果,这里是通过apply
//将gouzao函数内部的this指向修改成指向res,即实例对象
var ret = func.apply(res, Array.prototype.alice.call(args, 1)); 
}
if((typeof ret === "object" || typeofret === "function") && ret !== null) {
   return ret;
}
//使用自己封装的New方法创建对象
var Person = function(name) {
   this.name = name;
}
Person.protype.getName = function() {
   return this.name;
}
var p1 = New(Perosn, 'Jack');
var p2 = New(Person, 'Joe');
p1.getName();//Jack
p2.getName();//Joe

以下是构造函数、原型和实例之间的关系:
图片说明
由上图可得,构造函数的prototype与所有实例的proto都指向原型对象,而原型对象的constructor则指向构造函数。
因为在构造函数中声明的变量与方法只属于当前实例,因此我们可以将构造函数中声明的属性与方法称为该实例的私有属性和方法,它们只能被当前实例访问。
而原型中的方法与属性能够被所有的实例访问,因此我们将原型中声明的属性与方法称为公有属性与方法。
与在原型中添加一个方法不同,当在构造函数中声明一个方法时,每创建一个实例,该方法都会被重新创建一次。而原型中的方法仅仅只会被创建一次。
因此在构造函数中,声明私有方***消耗更多的内存空间。
如果再构造函数中声明的私有方法/属性与原型中的公有方法/属性重名,那么会优先访问私有属性/方法。

四、原型链
--proto--,就是构造函数的prototype里面的隐藏属性,所以对象的proto属性时指向他的原型。
另外原型对象的constructor属性则指向构造函数,用在遍历一个对象的时候,判断某个属性是不是对象本身的属性(HasOwnProperty),来提高程序的鲁棒性。

当想要判断一个对象foo是否是构造函数Foo的实例时,可以使用instanceof关键字。

foo instanceof Foo; // true: foo是Foo的实例,false:不是

当创建一个对象时,可以使用new Object()来创建。因此Object其实是一个构造函数,而其对应的原型Object.prototype则是原型链的终点。

Object.prototype.__proto__ === null // 所有的函数与对象都有一个toString与vallueOf方法,就是来自于Object.prototype
Object.prototype.toString = function () {} 
Object.prototype.valueOf = function () {}

当创建函数时,除可以使用function关键字外,还可以使用Function对象:

var add = new Function('a', 'b', 'return a+ b');
// 等价于
var add = function (a, b) {
  return a + b;
}

因此这里创建的add方法是一个实例,它对应的构造函数是Function,它的原型是Function.prototype。

add.__proto__ === Function.prototype; // true

需要注意的是,当构造函数与原型拥有同名的方法/属性时,如果用创建的实例访问该方法/属性,则优先访问构造函数的方法/属性

function Person (name) {
this.name = name; 
this.getName = function () { return 'name in Person'; }
} 
Person.prototype.getName = function () { return 'name in Person.prototype'; } var p1 = new Person('alex');
p1.getName(); // name in Person

我们用一张原型链图解来说明下原型链(顺序:上→右→左→下):
图片说明

我们可以说思考一些问题:
1.创建一个对象的时候,可以说还有new Object()来创建,因此Object其实是一个构造函数,对应的原型是Object.prototype是原型链的终点。

2.Object.prototype相当于一个大工厂,那么我们也是有办法创造一个级别和Object.prototype一样的对象,而且这个对象是真正的‘纯的’对象,他里面没有proto,是全空的,不像我们用字面量{ }创建的对象那样,还有一个proto指向构造类Object。这样,我们可以对他大肆使用for in,而且也不用hasOwnProperty了。

五、实例方法、原型方法、静态方法
构造函数中的方法称之为实例方法,通过prototype添加的方法,将会挂载到原型上,称之为原型方法,被直接挂在在构造函数上的方法称之为静态方法。
静态方法不能通过实例访问,只能沟通过构造函数来访问。

function Foo () { 
this.bar = function () { return 'bar in Foo'; // 实例方法 } 
} 
Foo.bar = function () { return 'bar in static'; // 静态方法 } 
Foo.prototype.bar = function () { return 'bar in prototype'; // 原型方法 }

六、继承
继承被分为两种,一种是有构造函数的继承,一种是原型继承。
假设已经封装好了一个父类对象Person。

var Person = function (name, age) { this.name = name; this.age = age; }
Person.prototype.getName = function () { return this.name; }
Person.prototype.getAge = function () { return this.age; }

构造函数的继承比较简单,可以借助call/apply来实现。假设想要通过继承封装一个Student的子类对象,那么构造函数的实现如下。

var Student = function (name, age, grade) {
 // 通过call方法还原Person构造函数中的所有处理逻辑 
Student.call(Person, name, age);
this.grade = grade; 
} 
// 等价于
var Student = function (name, age, grade) { 
this.name = name;
this.age = age;
this.grade = grade;
}

原型的继承则需要一点思考。首先应该考虑,如何将子类对象的原型加到原型链中?其实只需让子类对象的原型成为父类对象的一个实例,然后通过proto访问富磊对象的原型,这样就继承了父类原型中的方法与属性了。
可以先封装一个方法,该方***根据父类对象的原型创建一个实例,该实例即为子类对象的原型。

function create (proto, options) { 
// 创建一个空对象 
var tmp = {}; // 让这个新的空对象成为父类对象的实例 
tmp.__proto__ = proto; // 传入的方法都挂载到新对象上,新对象将作为子类对象的原型 Object.defineProperties(tmp, options);
return tmp; 
}

在简单封装了create方法之后,就可以使用该方法来实现原型的继承了。

Student.prototype = create(Person.prototype, { 
// 不要忘了重新指定构造函数 
constructor: { value: Student },
getGrade: { value: function () { return this.grade } 
} 
})
//验证正确性
var s1 = new Student('ming', 22, 5);
s1.getName(); // ming
s1.getAge(); // 22
s1.getGrade(); // 5

七、属性类型
在ES5中,对每个属性都添加了几个属性类型,用来描述这些属性的特点。
configurable:表示该属性是否能被delete删除。当其值为false时,其他的特性也不能被改变,默认为true。
enumerable:是否能枚举。即是否能被for-in遍历,默认为true。
writable:是否能修改值,默认为true。
value:该属性的具体值是多少,默认是undefined。
get:当通过person.name访问name属性的值时,get将被调用。该方法可以自定义返回的具体值是多少,get的默认值为undefined。
set:当通过person.name = 'Jake'设置name的值时,set方法将被调用。该方法可以自定义设置值的具体方式,set的默认值为undefined。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务