【读书笔记】《你不知道的JavaScript(上卷)》——第二部分 this和对象原型(三)

2021/7/1 12:21:32

本文主要是介绍【读书笔记】《你不知道的JavaScript(上卷)》——第二部分 this和对象原型(三),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 第4章 混合对象“类”
    • 4.1 类理论
      • 4.1.1 “类”设计模式
      • 4.1.2 JavaScript中的“类”
    • 4.2 类的机制
      • 4.2.1 建造
      • 4.2.2 构造函数
    • 4.3 类的继承
      • 4.3.1 多态
      • 4.3.2 多重继承
    • 4.4 混入
      • 4.4.1 显式混入
        • 1.再说多态
        • 2.混合复制
        • 3.寄生继承
      • 4.4.2 隐式混入
    • 4.5 小结
  • 第5章 原型
    • 5.1 [[Prototype]]
      • 5.1.1 Object.prototype
      • 5.1.2 属性设置和屏蔽
    • 5.2 “类”
      • 5.2.1 “类”函数
        • 关于名称
      • 5.2.2 “构造函数”
        • 1.构造函数还是调用
      • 5.2.3 技术
        • 回顾“构造函数”
    • 5.3 (原型)继承
        • 检查“类”关系
    • 5.4 对象关联
      • 5.4.1 创建关联
        • Object.create()的polyfill代码
      • 5.4.2 关联关系是备用
    • 5.5 小结


第4章 混合对象“类”

类的设计模式:实例化(instantiation)、继承(inheritance)和(相对)多态(polymorphism)

但是,这些概念实际上无法直接对应到JavaScript的对象机制

4.1 类理论

  • 类/继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法。
  • 面向对象编程强调的是数据和操作数据的行为的关联性,数据以及和它相关的行为打包即封装。
  • 继承:在原有基础上新增功能或属性
  • 实例化:把一个非实体对象的类实体化,让其有具体属性或功能
  • 多态:父类的通用行为可以被子类用更特殊的行为重写。

类理论强烈建议父类和子类使用相同的方法名来表示特定的行为,从而让子类重写父类。 但在JavaScript中这样会降低代码的可读性和健壮性。

4.1.1 “类”设计模式

类是一种设计模式,是一种可选的代码抽象,但在有些语言中是必选的,比如Java——万物皆是类;其他语言(比如C/C++或者PHP)会提供过程化和面向类这两种语法,开发者可以选择其中一种风格或者混用两种风格。

4.1.2 JavaScript中的“类”

ES6之前,JavaScript只有一些近似类的语法元素(比如new和instanceof),后来的ES6中新增了比如class关键字,这才使JavaScript有了近似类的语法,但JavaScript的机制其实和类完全不同!

4.2 类的机制

类是对所有实例化对象的抽象描述,类必须实例化之后才能直接使用

4.2.1 建造

类的概念来源于建造,类就相当于建造中的蓝图(类似现在的3d模型),是抽象的,而依据这个蓝图建造的过程就是类实例化的过程。

4.2.2 构造函数

类构造函数通常和类同名,且大多需要用new来调用。它的任务就是初始化实例需要的所有信息(状态)

4.3 类的继承

  • 子类可以重写父类的功能或新增新功能。

4.3.1 多态

  • 在继承链的不同层次中一个方法名可以被多次定义,当调用方法时会自动选择合适的定义(取决于具体调用者或参数)
  • 多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。

4.3.2 多重继承

JavaScript并不支持多重继承,因为使用多重继承的代价太大

4.4 混入

4.4.1 显式混入

// 非常简单的mixin(..)例子:
let mixin = (sourceObj, targetObj) => {
    for (let key in sourceObj) {
      // 只会在不存在的情况下复制
      if (! (key in targetObj)) {
          targetObj[key] = sourceObj[key];
      }
    }
    return targetObj;
}
let Chicken = {
	name: 'Chicken',
	fly: false,
	say: function() { console.log(`I am ${this.name}, I can ${this.fly?'':'not'}fly!`) },
}
let FlyingFish = mixin(Chicken , {
	name: 'FlyingFish',
	fly: true,
	say: function() { Chicken.say.call(this) },
})
Chicken.say() // I am Chicken, I can not fly!
FlyingFish.say() // I am FlyingFish, I can fly!

1.再说多态

代码中:Chicken.say.call(this),就是显式多态。

在JavaScript中使用显式伪多态会在所有需要使用(伪)多态引用的地方创建一个函数关联,这会极大地增加维护成本。此外,由于显式伪多态可以模拟多重继承,所以它会进一步增加代码的复杂度和维护难度。
使用伪多态通常会导致代码变得更加复杂、难以阅读并且难以维护,因此应当尽量避免使用显式伪多态,因为这样做往往得不偿失。

2.混合复制

若向目标对象中显式混入超过一个对象,就可以部分模仿多重继承行为,但是仍没有直接的方式来处理函数和属性的同名问题。即使是“晚绑定”技术,从根本上来说,在性能上还是得不偿失。

3.寄生继承

显式混入模式的一种变体被称为“寄生继承”,它既是显式的又是隐式的。

function Chicken() {
	this.name = 'Chicken'
}
Chicken.prototype.fly = false
Chicken.prototype.say = function() { 
	console.log(`I am ${this.name}, I can${this.fly?'':'not'} fly!`) 
}

function FlyingFish(){
	let flyingFish = new Chicken()
	flyingFish.name = 'FlyingFish'
	flyingFish.fly = true
	let fishSay = flyingFish.say
	flyingFish.say = function() { fishSay.call(this) }
	return flyingFish
}
let myChicken = new Chicken()
let myFlyingFish = new FlyingFish()
myChicken.say() // I am Chicken, I can not fly!
myFlyingFish.say() // I am FlyingFish, I can fly!

4.4.2 隐式混入

隐式混入和之前提到的显式伪多态很像,因此也具备同样的问题。

let Something = {
    cool: function() {
      this.greeting = "Hello World";
      this.count = this.count ? this.count + 1 : 1;
    }
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1

let Another = {
    cool: function() {
      // 隐式把Something混入Another
      Something.cool.call(this);
    }
};

Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1(count不是共享状态)

利用了this的重新绑定功能,但是Something.cool.call(this)仍然无法变成相对(而且更灵活的)引用,所以使用时千万要小心。通常来说,尽量避免使用这样的结构,以保证代码的整洁和可维护性。

4.5 小结

  • 类是一种设计模式。
  • JavaScript的类和其他语言中的类完全不同。
  • 类意味着复制。
  • 混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,这会让代码更加难懂并且难以维护。

此外,显式混入实际上无法完全模拟类的复制行为,因为对象(和函数!别忘了函数也是对象)只能复制引用,无法复制被引用的对象或者函数本身。忽视这一点会导致许多问题。总地来说,在JavaScript中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋下更多的隐患。

第5章 原型

5.1 [[Prototype]]

  • JavaScript中的对象的[[Prototype]]内置属性,其实就是对于其他对象的引用。
  • 几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。
  • 在进行属性查找时,对于默认的[[Get]]操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象的[[Prototype]]链:
var anotherObject = {
    a:2
};

// 创建一个关联到anotherObject的对象
var myObject = Object.create(anotherObject);

myObject.a; // 2
  • 任何可以通过原型链访问到(并且是enumerable)的属性都会被枚举;
  • 使用in操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)

5.1.1 Object.prototype

  • 所有普通的[[Prototype]]链最终都会指向内置的Object.prototype;
  • 这个Object.prototype对象,所以它包含JavaScript中许多通用的功能;

5.1.2 属性设置和屏蔽

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值:

  • 首先,会进行查找操作(直接对象中找不到的话会直到原型链中查找)
  • 找到即赋值,找不到就创建并赋值

在原型链上找到的情况比较特殊,分三种情况:

  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且writable: true,那就会直接在myObject中添加一个名为foo的新属性,它是 屏蔽属性
  2. 如果在[[Prototype]]链上层存在foo,但是它 writable:false ,那么无法修改已有属性或者在myObject上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽
  3. 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setterfoo不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo这个setter

若想在第二种和第三种情况下也屏蔽foo,那就不能使用=操作符来赋值,而是使用Object.defineProperty(…)

有些情况下会隐式产生屏蔽,一定要当心:

let anotherObject = {
    a:2
};

let myObject = Object.create(anotherObject);

anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("a"); // false

anotherObject.a+=1; // 隐式屏蔽!

anotherObject.a; // 2
myObject.a; // 3

myObject.hasOwnProperty("a"); // true
  • ++操作首先会通过[[Prototype]]查找属性a并从anotherObject.a获取当前属性值2,然后给这个值加1,接着用[[Put]]将值3赋给myObject中新建的屏蔽属性a。。。
  • 若是在未对myObject.a操作之前,对anotherObject.a进行操作的话,影响依旧会体现在myObject.a

5.2 “类”

  • JavaScript中只有对象
  • JavaScript才是真正应该被称为“面向对象”的语言,因为它是少有的可以不通过类,直接创建对象的语言

5.2.1 “类”函数

在JavaScript中,不能创建一个类的多个实例,只能创建多个对象,它们[[Prototype]]关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。

function Foo() {
}
console.log(Foo.prototype)	// { constructor: Foo() }
let foo = new Foo()
console.log(Object.getPrototypeOf(foo) === Foo.prototype) // true

new 关键字只是创建了一个关联到其他对象的新对象。

关于名称

  • 委托 这个术语可以更加准确地描述JavaScript中对象的关联机制。
  • 差异继承,基本原则是在描述对象行为时,使用其不同于普遍描述的特质。
  • 原型继承被视为动态语言版本的类继承,

5.2.2 “构造函数”

prototype默认有一个公有并且不可枚举的属性constructor

1.构造函数还是调用

  • 在JavaScript中对于“构造函数”最准确的解释是,带new函数调用
  • 函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”

5.2.3 技术

function Foo(name) {
    this.name = name; // 像类实例封装的数据值
}

Foo.prototype.myName = function() {
    return this.name;
};

var a = new Foo("a");
var b = new Foo("b");

a.myName(); // "a"
b.myName(); // "b"

回顾“构造函数”

function Foo() { /* .. */ }

Foo.prototype = { /* .. */ }; // 创建一个新原型对象(相应constructor也随之改变)

var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
  • .constructor是一个非常不可靠并且不安全的引用。通常来说要尽量避免使用这些引用

如果在使用过程中,不小心或是必须修改默认prototype,后续要用到constructor 时,记得修复:

Object.defineProperty(Foo.prototype, "constructor" , {
    enumerable: false,
    writable: true,
    configurable: true,
    value: Foo // 让.constructor指向Foo
} );

5.3 (原型)继承

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

Foo.prototype.myName = function() {
    return this.name;
};

function Bar(name, label) {
    Foo.call(this, name);
    this.label = label;
}

// 创建了一个新的Bar.prototype对象并关联到Foo.prototype
Bar.prototype = Object.create(Foo.prototype);

// 注意!现在没有Bar.prototype.constructor了
// 如果需要这个属性的话可能需要手动修复一下它

Bar.prototype.myLabel = function() {
    return this.label;
};

let a = new Bar("a", "obj a");

a.myName(); // "a"
a.myLabel(); // "obj a"

这段代码的核心部分就是:Bar.prototype = Object.create(Foo.prototype)
创建一个新的Bar.prototype对象并把它关联到Foo. prototype

注意,下面这两种方式是常见的错误做法,实际上它们都存在一些问题:

// 和想要的机制不一样!
Bar.prototype = Foo.prototype;

// 基本上满足需求,但是可能会产生一些副作用:(
Bar.prototype = new Foo();
  • Bar.prototype = Foo.prototype,这个操作很危险,这只是对象的软拷贝,Bar.prototype的修改会附加到Foo.prototype。。。
  • Bar.prototype = new Foo()的确会关联到Foo.prototype,除此之外Foo的其他Bar并不需要的属性或功能也会附加到Bar,后果不堪设想。。。

ES6添加了辅助函数Object.setPrototypeOf(…),可以用标准并且可靠的方法来附加关联:

// ES6之前需要抛弃默认的Bar.prototype
Bar.ptototype = Object.create(Foo.prototype);

// ES6开始可以直接修改现有的Bar.prototype
Object.setPrototypeOf(Bar.prototype, Foo.prototype);

检查“类”关系

在传统的面向类环境中,检查一个实例的继承祖先通常被称为内省(或者反射)。

function Foo() {
    // ...
}

Foo.prototype.blah = ...;

var a = new Foo();

方法一:

a instanceof Foo; // true

instanceof回答的问题是:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象

// 用来判断o1是否关联到(委托)o2的辅助函数
function isRelatedTo(o1, o2) {
    function F(){}
    F.prototype = o2;
    return o1 instanceof F;
}

var a = {};
var b = Object.create(a);

isRelatedTo(b, a); // true

isRelatedTo(..)内部我们声明了一个一次性函数F,把它的.prototype重新赋值并指向对象o2,然后判断o1是否是F的一个“实例”。显而易见,o1实际上并没有继承F也不是由F构造,所以这种方法非常愚蠢并且容易造成误解。

第二种判断[[Prototype]]反射的方法,它更加简洁:

Foo.prototype.isPrototypeOf(a); // true

isPrototypeOf(..)回答的问题是:在a的整条[[Prototype]]链中是否出现过Foo.prototype

可以直接获取一个对象的[[Prototype]]链:

Object.getPrototypeOf(a);

所以还可以这么干:

Object.getPrototypeOf(a) === Foo.prototype; // true

绝大多数(不是所有!)浏览器也支持一种非标准的方法来访问内部[[Prototype]]属性:

a.__proto__ === Foo.prototype; // true

这个奇怪的.__proto__(在ES6之前并不是标准!)属性“神奇地”引用了内部的[[Prototype]]对象,甚至可以通过.__proto__.__ptoto__..来遍历

.constructor一样,.__proto__实际上并不存在于对象中。实际上,它和其他的常用函数(.toString().isPrototypeOf(..),等等)一样,存在于内置的Object.prototype中。(它们是不可枚举的)

此外,.__proto__看起来很像一个属性,但是实际上它更像一个getter/setter.__proto__的实现大致上是这样的:

Object.defineProperty(Object.prototype, " __proto__", {
    get: function() {
      return Object.getPrototypeOf(this);
    },
    set: function(o) {
      // ES6中的setPrototypeOf(..)
      Object.setPrototypeOf(this, o);
      return o;
    }
} );

5.4 对象关联

5.4.1 创建关联

Object.create(..)会创建一个新对象并通过赋值的方式把它关联到指定的对象,这样就可以充分发挥[[Prototype]]机制的威力(委托)并且避免不必要的麻烦(比如使用new的构造函数调用会生成.prototype.constructor引用)

Object.create(null)会创建一个拥有空[[Prototype]]链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以instanceof操作符无法进行判断,因此总是会返回false。这些特殊的空[[Prototype]]对象通常被称作“字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据。

Object.create()的polyfill代码

Object.create(..)是在ES5中新增的函数,所以在ES5之前的环境中(比如旧IE)如果要支持这个功能的话就需要使用一段简单的polyfill(补丁)代码,它部分实现了Object. create(..)的功能:

if (! Object.create) {
    Object.create = function(o) {
      function F(){}
      F.prototype = o;
      return new F();
    };
}

标准ES5中内置的Object.create(..)函数还提供了一系列附加功能:

var anotherObject = {
    a:2
};

var myObject = Object.create(anotherObject, {
    b: {
      enumerable: false,
      writable: true,
      configurable: false,
      value: 3
    },
    c: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: 4
    }
});

myObject.hasOwnProperty("a"); // false
myObject.hasOwnProperty("b"); // true
myObject.hasOwnProperty("c"); // true

myObject.a; // 2
myObject.b; // 3
myObject.c; // 4

Object.create(..)的第二个参数指定了需要添加到新对象中的属性名以及这些属性的属性描述符。因为ES5之前的版本无法模拟属性操作符,所以polyfill(补丁)代码无法实现这个附加功能。

5.4.2 关联关系是备用

var anotherObject = {
    cool: function() {
      console.log("cool! ");
    }
};

var myObject = Object.create(anotherObject);

myObject.cool(); // "cool! "

只是为了让在无法处理属性或者方法时可以有备用使用,那么这段程序就会变得有点“神奇”,而且很难理解和维护。

在ES6中有一个被称为“代理”(Proxy)的高端功能,它实现的就是“方法无法找到”时的行为

这个功能完全可以使用 内部委托机制 实现:

var anotherObject = {
    cool: function() {
      console.log("cool! ");
    }
};

var myObject = Object.create(anotherObject);

myObject.doCool = function() {
    this.cool(); // 内部委托!
};

myObject.doCool(); // "cool! "

5.5 小结

  • 若要访问对象中并不存在的一个属性,[[Get]]操作就会查找对象内部[[Prototype]]关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历。
  • 所有普通对象都有内置的Object.prototype,指向原型链的顶端(比如说全局作用域),如果在原型链中找不到指定的属性就会停止。toString()valueOf()和其他一些通用的功能都存在于Object.prototype对象上,因此语言中所有的对象都可以使用它们。
  • 关联两个对象最常用的方法是使用new关键词进行函数调用,在调用过程中会创建一个关联其他对象的新对象。
  • 使用new调用函数时会把新对象的.prototype属性关联到“其他对象”。
  • 对象之间的关系不是复制而是委托。



这篇关于【读书笔记】《你不知道的JavaScript(上卷)》——第二部分 this和对象原型(三)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程