第六章 面向对象的程序设计(js高级程序设计)

Object-Oriented 面向对象

理解对象

  • 对象属性分为 【数据属性】 和 【访问器属性】
  • 对象属性中的【数据属性】
    1. configurable 描述该属性能否通过delete删除或被重新定义(为false时defineProperty也无法使用了)
    2. enumerable 能否被for-in循环返回属性
    3. writable 能否修改属性的值
    4. value 这个属性的数据值
  • 利用defineProperty来定义数据属性


    因为writable属性被设置为false,我尝试修改xusheng的值,但是无效
  • 对象属性中的的 【访问器属性】
  1. configurable 描述该属性能否通过delete删除或被重新定义(为false时defineProperty也无法使用了)
  2. enumerable 能否被for-in循环返回属性
  3. get 在读取属性时调用的函数。
  4. set 在写入属性时调用的函数。
当获取_name值时,调用get方法得到name的值。当设置_name值时,其实时将name值改为了‘默认值’

_ name 前面 _ 的书写代表只能通过对象方法访问的属性。

  • 定义多个属性:defineProperties可以创建多个对象属性


    例子创建了_year和edition两个数据属性和year访问器属性
  • 读取属性的特性
    通过getOwnPropertyDescriptor获取到的上面例子的属性描述符
    var descriptor = Object.getOwnPropertyDescriptor(book, "_year");

上述book对象的_ year属性的属性描述符
  • 创建对象方法:
    1. new Object()
    2. 对象字面量{ }
      缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

其他创建对象的方法

1. 工厂模式
这种模式抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。

工厂模式用函数封装了创建对象的过程

缺点:工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

2. 构造函数模式
构造函数可以用来创建特定类型的对象

书中的案例

  • 构造函数开头字母需要大写(Person)

  • 在new 构造函数时,程序会执行以下步骤

    1. 创建一个新对象
    2. 将构造函数的作用域赋值给新对象
    3. 执行构造函数中的代码
    4. 返回新对象
  • 对象的 constructor 是用来标识对象类型的

  • 通过构造函数创建对象有一个constructor属性,指向原构造函数

  • 创建得对象是构造函数的实例


    实例和构造函数关系
  • 以上构造函数带来的问题,相同作用的函数被反复复制,改进:

sayName其实做了同一件事,但是却被创建成了两个不同的function,浪费
将sayName拎出来
  • 新的问题:全局下又不可能有多个类似于sayName函数的东西,会很乱

3. 原型模式

关系图
实例的sayName均来自构造函数的原型
构造函数同样实现效果

回到问题本质,与构造函数的区别在哪里?
新对象的这些属性和方法是由所有实例共享(不会再创建相同作用的函数)的。换句话说,person1 和 person2 访问的都是同一组属性和同一个 sayName()函数

  • 理解原型对象
我的关系图:person1实例与构造函数没有直接关系,person1实例可以调用sayName是因为他在原型对象上进行查找.!注意这里有个错误:应该是__proto__。两个下划线!
书中关系图
  • __proto__ 是两个下划线!!

  • 确定实例和原型对象关系可以用:

  1. isPrototypeOf()判断对象是否和原型对象有关
  2. Object.getPrototypeOf()获得对象的原型对象

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true

Object.getPrototypeOf(person1) == Object.getPrototypeOf(person1) //获得person1对象指向的原型对象

  • 当调用person1.sayname时。解析器会现在实例对象上查找sayname属性,如果找不到,再在原型对象上查找sayname属性。所以实例上的属性会屏蔽掉原型上同名属性

  • 通过delate操作符可以删除实例上的属性,从而恢复原型的访问

  • 利用hasOwnProperty(“属性名”)可以判断这个属性是来自于原型对象还是自己本身

person1实例上name属性来自本身还是来自于原型对象上
  • 与hasOwnProperty不同的是,使用in操作符只要能访问到该属性都会返回true(无论是原型对象上还是实例对象上)
alert(person1.hasOwnProperty("name")); //false 
alert("name" in person1); //true name属性在实例对象属性中
  • hasPrototypeProperty(person1,"name") 实例对象要能访问到原型上的属性才能返回true,即使原型对象上有属性name,但是实例对象上的name将原型对象上的name屏蔽,所以依然返回false
var person = new Person(); 
alert(hasPrototypeProperty(person, "name")); //true  
person.name = "Greg"; 
alert(hasPrototypeProperty(person, "name")); //false  实例对象要能访问到原型上的属性才能返回true
  • for in 枚举属性
    原则:所有开发人员定义的属性都是可以枚举的属性,所以对象实例属性和原型对象属性都是可以被枚举的。


    图中红色框中属性都可被枚举,黄色框中不可被枚举,因为他们来自原型链中的Object原型属性
  • 取得对象上所有可枚举的实例属性:Object.keys()

  • 使用Object.key()取得对象上可枚举的属性组成的字符串数组

var person = new Person()
Object.key(person) //"say"  person实例对象上的属性,原型对象上的不会被
console.log(Object.keys(person.__proto__))// "age,job,name,sayName" 原型上的属性
  • Object.getOwnPropertyNames(object) 可以列出对象上所有的属性,即使是不可枚举的属性,你可以试着枚举出继承的Object对象上的属性
console.log(Object.getOwnPropertyNames(person.__proto__.__proto__))
  • 换一种方式定义构造函数原型上的属性
function Person(){ 
} 
Person.prototype = { 
 constructor : Person,  //如果没有这句,constructor 属性就指不到构造函数了
 name : "Nicholas", 
 age : 29, 
 job: "Software Engineer", 
 sayName : function () { 
 alert(this.name); 
 } 
}; 

此方法带来的问题:constructor 的[[Enumerable]]值为true可枚举了

使用Object.defineProperty重设构造函数

Object.defineProperty(Person.prototype, "constructor", { 
 enumerable: false, 
 value: Person 
}); //此写法适用于es5
  • 原型的动态性:很好理解,实例对象(person1)的prototype属性指针指向原型对象,所以在原型对象上多次修改属性值,在实例上也同样能被正确的访问
var friend = new Person(); 
Person.prototype.sayHi = function(){ 
 alert("hi"); 
}; 
friend.sayHi(); //"hi"(没有问题!)

当然,你不能重写原型对象,切断与构造函数的联系

function Person(){ 
} 
var friend = new Person(); 

Person.prototype = { 
 constructor: Person, 
 name : "Nicholas", 
 age : 29, 
 job : "Software Engineer", 
 sayName : function () { 
 alert(this.name); 
 } 
}; 
friend.sayName(); //error 
在new 构造函数时,构造函数会为实例创造指针__proto__指向原型对象。重写原型后,旧的实例依然和旧的原型保持着联系,和新的原型没有联系
  • 原生对象的原型:原生(Array,String)构造函数都在其原型(prototype)上定义了方法,所以我们可以通过例如String.substring()来操作字符串实例
String.prototype.startsWith() =function(){
  ...//你可以给原生对象添加属性,让所有实例化的字符串都能访问,但是不建议这样做
}

  • 原型对象的问题:修改原型对象上的属性值会对其他实例对象造成影响(我们有时候需要它[所有string的实例都能使用String.substring()],有时候又不需要它)

4. 组合使用构造函数和原型模式

  • 书中想表达的意思就是物尽其用,将实例属性放到构造函数中定义,将用于共享的属性和方法放到原型对象里
function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.friends = ["Shelby", "Court"]; 
} 
Person.prototype = { 
 constructor : Person, 
 sayName : function(){ 
 alert(this.name); 
 } 
} 

5. 动态原型模式:在构造函数中使用原型模式,并且避免重复挂载到原型模式
它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点
换句话说,第一次new Person()的时候,sayName肯定是不存在的,所以会挂载sayName和其他方法到原型上,而下一次newPerson的时候,sayName()已经在原型上了[注意if判断的作用],sayName包括其他if语句中的方法都不会被重复挂载

  function Person(name, age, job) {
    //属性
    this.name = name;
    this.age = age;
    this.job = job; //方法
    if (typeof this.sayName != "function") {
      Person.prototype.sayName = function () {
        alert(this.name);
      };

      Person.prototype.sayAge = function () {
        alert(this.age);
      };
    }
  }

6. 寄生构造函数模式
类似于工厂模式,不过使用new function()的形式创建

function Person(name, age, job){ 
 var o = new Object(); 
 o.name = name; 
 o.age = age; 
 o.job = job; 
 o.sayName = function(){ 
 alert(this.name); 
 }; 
 return o; 
} 
var friend = new Person("Nicholas", 29, "Software Engineer"); 
friend.sayName(); //"Nicholas" 

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊
数组。由于不能直接修改 Array 构造函数,因此可以使用这个模式
注意:返回的对象与构造函数或者与构造函数的原型属性之间没有关系。 instanceof 不能溯源,所以建议在可以使用其他模式的情况下,不要使用这种模式。

7. 稳妥构造函数模式(闭包)

function Person(name, age, job){ 
 
 //创建要返回的对象
 var o = new Object(); 
 //可以在这里定义私有变量和函数
 //添加方法
 o.sayName = function(){ 
 alert(name); 
 }; 
 //返回对象
 return o; 
} 
var friend = Person("Nicholas", 29, "Software Engineer"); 
friend.sayName(); //"Nicholas" 

除了使用 sayName()方法之外,没有其他办法访问 name 的值(有点用到了闭包,让匿名函数返回私有变量)
同样和寄生构造模式一样不能溯源,不能使用instanceof

继承

  • 函数签名(或者类型签名,抑或方法签名)定义了 函数或方法的输入与输出。JavaScript 是一种类型松散或动态语言。这意味着您不必提前声明变量的类型。以下是java的函数签名
public static void main(String[] args)
  • 继承分为接口继承和实现继承
    接口继承:接口是一种特殊的抽象类,即继承函数的签名,故js中没有接口继承
    实现继承:实现继承是继承函数实际的方法

  • 子类型超类型
    子类型:继承者
    超类型:被继承者(下文用父类型)

1.原型链

继承图解A继承B

  • 注意此时的 instance 应该是指向B的原型,因为A的prototype被b的实例重写了。

  • 在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上

  • 所有函数的默认原型都是 Object 的实例:原型链的底层永远都是Object。因此默认原型都会包含一个内部指针,指向 Object.prototype(也就是继承于Object原生)。这也是为什么所有自定义类型都会继承toString(),valueOf()方法的原因。上图的B其实还继承了原生Object,如下图。


    默认原型都会指向Object.prototype,
  • 确定原型和实例的关系的方式:
    1.instanceof

alert(instance instanceof Object); //true 
alert(instance instanceof SuperType); //true 
alert(instance instanceof SubType); //true
  1. isPrototypeOf
alert(Object.prototype.isPrototypeOf(instance)); //true 
alert(SuperType.prototype.isPrototypeOf(instance)); //true 
alert(SubType.prototype.isPrototypeOf(instance)); //true 
  • 在完成继承后再添加额外的方法,否则额外的方法会被继承覆盖掉。先.prototype = new Function(),再.prototype.xx=xx.更不能使用.prototype={ }字面量重写原型
  • 原型继承带来的引用类型的问题:原型中包含引用类型,A构造函数的原型继承于B的实例,基于A创建M实例,修改引用类型值,那么B的原型中的值也相应会改变,如果再基于A创建N实例,那么M,N就会拥有相同的引用。
    不能向超类型的构造函数传递参数

2.借用构造函数

  • 利用apply或者call在自己的构造函数中调用别人的构造函数
function SuperType(name){ 
 this.name = name; 
} 
function SubType(){ 
 //继承了 SuperType,同时还传递了参数
 SuperType.call(this, "Nicholas"); 
 
 //实例属性
 this.age = 29; 
} 
var instance = new SubType(); 
alert(instance.name); //"Nicholas"; 
alert(instance.age); //29 
  • 缺点很明显:方法都在构造函数中定义,因此函数复用就无从谈起。而且父类型原型中的方法子类型也用不了

3.组合继承

  • 组合继承是ji中常用的实现继承的方式,将原型继承和构造函数继承集合
  function f1(name) {
    this.name = name
    this.colors = ['blue', 'red', 'green'] //将需要被继承的引用类型定义在构造函数中
  }

  f1.prototype.sayName = function () {
    alert(this.name)
  }

  function f2(name, age) {
    f1.call(this, name)  //此时,f2上已经继承f1构造函数中的属性
    this.age = age
  }

  f2.prototype = new f1(); //此时,f2已经继承f1原型对象上的属性

  var F2 = new f2()

  for (const x in F2) {
    console.log(x)//name color age sayname
  }

4.原型式继承

  • 将一个对象作为子对象的原型对象实现继承
  function object(o) {
    function F() { }
    F.prototype = o; //person对象被共享到了通过object创造出来的对象的原型上
    return new F();
  }
  var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
  };
  debugger
  var anotherPerson = object(person);
  anotherPerson.name = "Greg";
  anotherPerson.friends.push("Rob");
  var yetAnotherPerson = object(person);
  yetAnotherPerson.name = "Linda";
  yetAnotherPerson.friends.push("Barbie");
  alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 
  • Es5规范化方法:Object.creat()
var person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = Object.create(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 
 
var yetAnotherPerson = Object.create(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式
继承是完全可以胜任的.

  • 个人理解:其实就是原型链继承的缩写方案,同样存在原型链继承引用类型被共享的问题

5.寄生式继承

  function createAnother(original) {
    var clone = Object.create(original); //通过调用函数创建一个新对象
    clone.sayHi = function () { //以某种方式来增强这个对象
      alert("hi");
    };
    return clone; //返回这个对象
  }
  var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
  };
  var anotherPerson = createAnother(person);
  anotherPerson.sayHi(); //"hi" 

注意点: clone.sayHi 可以给新对象上添加属性,和寄生构造函数模式有点像,createAnother存在的意义就是在新的实例上添加公有属性

6.寄生组合式继承

  • 还记得组合继承模式吗?通过f2.call(this)和.protype=new f2()分别调用了两次父类型,寄生组合式继承就是为了解决这个问题。
  • inheritPrototype接受两个参数,分别是子类型的构造函数和夫类型的构造函数
function inheritPrototype(subType, superType){ 
 var prototype = Object.creat(superType.prototype); //创建对象
 prototype.constructor = subType; //增强对象
 subType.prototype = prototype; //指定对象
} 
function SuperType(name){ 
 this.name = name; 
 this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function(){ 
 alert(this.name); 
}; 
function SubType(name, age){ 
 SuperType.call(this, name); 
 this.age = age; 
} 
inheritPrototype(SubType, SuperType);   //SubType.protoype = new SuperType();
SubType.prototype.sayAge = function(){ 
 alert(this.age); 
}; 
  • 只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性
  • 个人觉得是拷贝父类型的prototype到子类型的protype上,inheritPrototype的作用其实就是SubType.protoype = new SuperType();

ps:终于看完了,真特么累,感觉迷迷糊糊的,肯定要刷第二遍的~。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容