美文网首页
《JavaScript面向对象编程指南》笔记

《JavaScript面向对象编程指南》笔记

作者: 柠檬果然酸 | 来源:发表于2018-12-07 16:24 被阅读0次

P8


JavaScript 与 C++或 Java 这种传统的面向对象语言不同,它实际上压根儿没有类。该语言的一切都是基于对象的,其依靠的是一套原型(prototype)系统。而原型本身实际上也是一种对象。

封装
我们只需要知道所操作对象的接口,而不必去关心它的具体实现。

备注
在 JavaScript 中,尽管所有的方法和属性都是 public 的,但该语言还是提供了一些隐藏数据的方法,以保护程序的隐密性。

P9

聚合
将几个现有对象合并成一个新对象的过程。在 Java 中类的成员变量可以是基本类型(如 int、char),也可以是其他类。

继承 多态
和 Java 中的继承、多态一样

P23

基本数据类型
数字
字符串
布尔值
undefined:仅声明但未赋值。
null:已经赋值,只不过值为 null。用 typeof 查看变量类型输出的是 'object'。

P27

Infinity(无穷大)
在 JavaScript 中,还有一种叫做Infinity 的特殊值。它所代表的是超出了 JavaScript 处理范围的数值。但 Infinity 依然是一个数字,我们可以在控制台使用 typeof 来测试Infinity。当我们输入1e308 时,一切正常,但一旦将后面的308 改成 309 就出界了。实践证明,JavaScript所能处理的最大值是1.7976931348623157e+308,而最小值为5e-324。

另外,任何数除以 0 结果也为 Infinity

P28

NaN(Not A Number)
实上它依然属于数字类型。运算错误就会返回 NaN。

P30

字符串转数字
> var s = 100;
> s = s * 1;
> typeof s;
< "number"

P34

空字符串 ""
null
undefined
数字 0
数字 NaN
布尔值 false

这 6 个值有时也会被我们称为 falsy 值,而其他值则被称为 truthy 值(包括字符串""、" "、"false"等)。

P37

如果 JavaScript 引擎在一个逻辑表达式中遇到一个非布尔类型的操作数,那么该操作数的值就会成为该表达式所返回的结果。例如:
> true || "something";
< true

> true && "something";
< "something"

> true && "something" && true;
< true

通常情况下,这种行为应该尽量避免,因为它会使我们的代码变得难以理解。但在某些时候这样做也是有用的。例如,当我们不能确定某个变量是否已经被定义时,就可以像下面这样,即如果变量 mynumber 已经被定义了,就保留其原有值,否则就将它初始化为 10。
> var mynumber = mynumber || 10;
> mynumber;
< 10

这种做法简单而优雅,但是请注意,这也不是绝对安全的。如果这里的 mynumber 之前被初始化为 0(或者是那 6 个 falsy 值中的任何一个),这段代码就不太可能如我们所愿了。
> var mynumber = 0;
> var mynumber = mynumber || 10;
> mynumber;
< 10

P39

NaN 不等于任何东西,包括它自己
> NaN == NaN;
< false

undefined 与 null
当我们尝试使用一个不存在的变量时,控制台中就会产生以下错误信息:
> foo;
< ReferenceError: foo is not defined

但当对不存在的变量使用 typeof 操作符时则不会出现这样的错误,而是会返回一个字符串"undefined"。
> typeof foo;
< "undefined"

如果我们在声明一个变量时没有对其进行赋值,调用该变量时并不会出错,但 typeof操作符依然会返回"undefined":
> var somevar;
> typeof somevar;
< "undefined"

这是因为当我们声明而不初始化一个变量时,JavaScript 会自动使用 undefined 值来初始化这个变量。
> var somevar;
> somevar === undefined;
< true

但 null 值就完全是另一回事了。它不能由 JavaScript 自动赋值,只能交由我们的代码来完成。
> var somevar = null;
> somevar;
< null

> typeof somevar;
< "object"

P43

如果新元素被添加的位置与原数组末端之间存在一定的间隔,那么这之间的元素将会被自动设定为 undefined 值。例如:
> var a = [1,2,3];
> a[6] = 'new';
> a;
< [1, 2, 3, undefined x 3, "new"]

P44

删除元素
为了删除特定的元素,我们需要用到 delete 操作符。然而,相关元素被删除后,原数组的长度并不会受到影响。从某种意义上来说,该元素被删除的位置只是被留空了而已。
> var a = [1,2,3];
> delete a[1];
> a;
< [1, undefined, 3]

P45

我们也可以通过这种数组访问方式来获取字符串中特定位置上的字符
> var s = 'one';
> s[0];
< "o"

P63

调用函数时忘了传递相关的参数,JavaScript 引擎就会自动将其设定为 undefined。

内建变量 arguments
它能返回函数所接收的所有参数。

> function args() {
     return arguments;
  }
> args();
< []
> args( 1, 2, 3, 4, true, 'ninja');
< [1, 2, 3, 4, true, "ninja"]

P65

预定义函数

parseInt()
将字符串转换为整数,支持 8 进制、10 进制和 16 进制。

parseFloat()
将字符串转换为十进制数

isNaN()
用与判断是否是 NaN

isFinite()
判断是否是 Infinity

P69

URI 的编码与反编码
在 URL(Uniform Resource Locator,统一资源定位符)或 URI(Uniform ResourceIdentifier,统一资源标识符)中,有一些字符是具有特殊含义的。如果我们想“转义”这些字符,就可以去调用函数 encodeURI()或 encodeURIComponent()。
> var url = 'http://www.packtpub.com/scr ipt.php?q=this and that';
> encodeURI(url);
< "http://www.packtpub.com/scr%20ipt.php?q=this%20and%20that"

> encodeURIComponent(url);
< "http%3A%2F%2Fwww.packtpub.com%2Fscr%20ipt.php%3Fq%3Dthis%20and%20that"

encodeURI()和 encodeURIComponent()分别都有各自对应的反编码函数:decodeURI() 和 decodeURIComponent()。

eval()
eval()会将其输入的字符串当做 JavaScript 代码来执行。
> eval('var ii = 2;');
> ii;
< 2

所以,这里的 eval('var ii = 2;')与表达式 var ii = 2;的执行效果是相同的。
安全性方面 — JavaScript 拥有的功能很强大,但这也意味着很大的不确定性,如果您对放在 eval()函数中的代码没有太多把握,最好还是不要这样使用。
性能方面 — 它是一种由函数执行的“动态”代码,所以要比直接执行脚本要慢。

总结下来就是功能强大但不安全,而且执行速度慢,所以最好别用。

P72

变量提升

> var a = 123;
> function f() {
      alert(a);
      var a = 1;
      alert(a);
  }
> f();

这串代码执行的结果是:
第一个 alert() 弹出 undefined
第二个 alert() 弹出 1

尽管第一次调用 alert() 时变量 a 还没有被正式定义,但该变量已经存在于本地空间了,而且函数域始终优于全局域,所以第一次弹出 undefined。

其实我觉得变量提升这个特性不用去记,这种容易造成误解的特性一般不会有人去用。
更多参考JavaScript中变量提升是语言设计缺陷

P73

函数也是数据
这种特殊的数据有两个重要特性:
1.它们所包含的是代码
2.它们是可执行的

举个栗子,我们可以把一个函数赋值给一个变量

> function define() {
      return 1;
  }
> var express = function() {
      return 1;
  }
> typeof define;
< "function"
> typeof express;
< "function"

像变量那样使用函数

> var sum = function(a, b) {
      return a + b;
  }
> var add = sum;
> typeof add;
< "function"
> add(1, 2);
< 3

P75

回调函数
既然函数是一种特殊的变量,那么它也能像变量那样被当成参数传给其它函数。

> function invokeAdd(a, b) {
      return a() + b();
  }
> invokeAdd(
      function () {return 1; },
      function () {return 2; }
  );
< 3

自己写了一个遍历数组的回调函数

> function each(array, callback) {
      for (var i = 0; i < array.length; i++) {
          callback(array[i]);
      }
  }
> var num = [1, 4, 77, 233, 2233];
> each(a, function(item) {
      alert(item);
  });

P79

即时函数
函数在定义后可以立即使用

> (function() {
      // ...
  }());

P80

内部(私有)函数
函数和其他类型本质上是一样的,所以函数内部也可以定义一个函数。

function outer(param) {
    function inner(theinput) {
        return theinput * 2;
    }
    return 'The result is ' + inner(param);
}

私有函数外部不可访问
> outer(2);
< "The result is 4"
> inner(2);
< ReferenceError: inner is not defined

P81

返回函数的函数
函数始终都会有一个返回值,即便不是显式返回,它也会隐式返回一个 undefined。既然函数的本质是变量,那么它自然能够作为值被返回。

function a() {
    alert('A!');
    return function(){
        alert('B!');
    };
}

执行函数
> var newFunc = a();
> newFunc();

如果您想让返回的函数立即执行,也可以不用将它赋值给变量,直接在该调用后面再加一对括号即可,效果是一样的:
> a()();

P82

重写函数
函数能够从内部重写自己

function a() {
    alert('A!');
    a = function(){
        alert('B!');
    };
}

执行这个函数的话除了第一次会弹出 A,之后只会弹出 B。这是因为在 a() 第一次执行前它内部是这样的

> console.info(a);
< function a() {
      alert('A!');
      a = function() {
          alert('B!');
      };
  }

函数调用一次之后内部就被重写了

> a();
> console.info(a);
< function() {
      alert('B!');
  };

重写函数有什么
不同的浏览器特性不同,我们可以通过重写让函数根据当前所在的浏览器来重定义自己。这就是所谓的“浏览器兼容性探测”技术。

P86

闭包
闭包最常见的例子就是利用闭包突破作用域链。在此之前有两个重要概念:
1.函数内的函数能够访问函数内的变量。
2.所有函数都能够访问全局变量。

从函数外部访问函数内的变量
首先在目标函数内声明一个函数,声明的这个函数对目标函数的所有变量拥有访问权限,然后再将声明的函数赋值给一个全局变量,这样就能做到从外部访问函数内的变量了。

闭包#1

> var F = function() {
      var b = 'local variable';
      var N = function() {
          return b;
      };
      return N;
  }
> var inner = F();
> inner();
< "local variable"

闭包#2

> var inner; // placeholder
> var F = function() {
      var b = 'local variable';
      var N = function() {
          return b;
      }
      inner = N;
  }
> F();
> inner();
< "local variable"

由于 N() 是在 F() 内部定义的,它可以访问 F() 的作用域,所以即使该函数后来升级成了全局函数,但它依然可以保留对 F() 作用域的访问权。

闭包#3
每个函数都可以被认为是一个闭包。因为每个函数都在其作用域中维护了某种私有联系。但在大多数时候,该作用域在函数体执行完之后就自行销毁了— 除非发生一些有趣的事(比如像上一小节所述的那样),导致作用域被保持。

让我们再来看一个闭包的例子。这次我们使用的是函数参数(function parameter)。该参数与函数的局部变量没什么不同,但它们是隐式创建的(即它们不需要使用 var 来声明)。

> function F(param) {
      var N = function() {
          return param;
      };
      param++;
      return N;
  }
> var inner = F(123);
> inner();
< 124;

循环中的闭包
新手们在闭包问题上会犯的典型错误

> function F(param) {
      var arr = [];
      for(var i = 0; i < 3; i++) {
          arr[i] = function() {
              return i;
          };
      }
      return arr;
  }
> var arr = F();
> arr[0]();
< 3
> arr[1]();
< 3
> arr[2]();
< 3

为什么返回的都是3
在这串代码中创建了3个闭包,它们都指向了同一个局部变量 i。而 return i; 是引用传递而不是值传递,传递的是 i 的引用而非 i 的值。执行完 F() 函数之后 i 的值为3,所以3个闭包输出的值都是3。

换一种闭包形式

> function F(param) {
      var arr = [];
      for(var i = 0; i < 3; i++) {
          arr[i] = (function(x) {
              return function() {
                  return x;
              }
          }(i) );
      }
      return arr;
  }
> var arr = F();
> arr[0]();
< 0
> arr[1]();
< 1
> arr[2]();
< 2

这串代码其实也没有改变引用传递的方式,只不过是创建了3个引用,而且用上了即时函数。

P91

getter 与 setter
为了不将数据暴露给外部,我们将数据封装在函数内部。为了操作数据,我们提供两个接口:getter 与 setter 来获取和设置值。

> var getValue, setValue;
> (function() {
      var secret = 0;
            
      getValue = function() {
          return secret;
      };
            
      setValue = function(v) {
          if(typeof v === 'number') {
              secret = v;
          }
      };
  }() );
> getValue();
< 0
> setValue(123);
> getValue();
< 123
> setValue(false);
> getValue();
< 123

P92

迭代器
我们都知道如何用循环来遍历一个简单的数组,但是有时候我们需要面对更为复杂的数据结构,它们通常会有着与数组截然不同的序列规则。这时候就需要将一些“谁是下一个”的复杂逻辑封装成易于使用的 next()函数,然后,我们只需要简单地调用 next() 就能实现对于相关的遍历操作了。

> function setup(x) {
      var i = 0;
      return function() {
          return x[i++];
      };
  }
> var next = setup(['a', 'b', 'c']);
> next();
< "a"
> next();
< "b"
> next();
< "c"

P93

练习题

1.颜色转换器

> function getRGB(color) {
      if(typeof color != 'string') {
          console.info('请输入字符串');
          return;
      }
            
      var reg = /^#?[0-9a-fA-F]{6}$/;
      if(reg.test(color)) {
          var rgb = color.match(/[0-9a-fA-F]{2}/g);
          var r = parseInt(rgb[0], 16);
          var g = parseInt(rgb[1], 16);
          var b = parseInt(rgb[2], 16);
          return 'rgb(' + r + ', ' + g + ', ' + b + ')';
      }
  }
> var str = '#334aF4';
> getRGB(str);
< "rgb(51, 74, 244)"

2.如果在控制台中执行以下各行,分别会输出什么内容?

> parseInt(1e1);
< 10
> parseInt('1e1');
< 1
> parseFloat('1e1');
< 10
> isFinite(0/10);
< true
> isFinite(20/0);
< false
> isNaN(parseInt(NaN));
< true

3.下面代码中,alert()弹出的内容会是什么?

> var a = 1;
> function f() {
      function n() {
          alert(a);
      }
      var a = 2;
      n();
  }     
> f();

会弹出2,应该是变量提升的缘故。

4.以下所有示例都会弹出"Boo!"警告框,您能分别解释其中原因吗?

4.1

var f = alert;
eval('f("Boo!")');

在 JavaScript 中函数也是变量,只不过这个变量包含的是代码并且可执行。将 alert 函数赋值给 f,相当于是给 alert 取了个别名,后面加 () 执行函数。直接调用 f('Boo!'); 也能够达到同样的效果。

4.2

var e;
var f = alert;
eval('e=f')('Boo!');

将函数 f 赋值给 e 并且在赋值完之后执行函数。

4.3

(function() {
    return alert;
})() ('Boo!');

把这串代码拆分来看,可以分成两个部分:

第一部分
(function() { return alert; })()
这一个即时函数,它返回的是 alert

第二部分
('Boo!');
这是一个方法体

它们合在一起就是 alert('Boo!');

P97

元素、属性、方法与成员
说到数组的时候,我们常说其中包含的是元素。而当我们说对象时,就会说其中包含的是属性。实际上对于 JavaScript 来说,它们并没有多大的区别,只是在技术术语上的表达习惯有所不同罢了。这也是它区别于其他程序设计语言的地方。

另外,对象的属性也可以是函数,因为函数本身也是一种数据。在这种情况下,我们称该属性为方法。例如下面的 talk 就是一个方法:

var dog = {
    name: 'Benji',
    talk: function() {
        alert('Woof, woof!');
    }
}

如果我们要访问的属性名是不确定的,就必须使用中括号表示法了,它允许我们在运行时通过变量来实现相关属性的动态存取。

> var key = 'name';
> dog[key];
< "Benji"

P101

修改属性与方法
由于 JavaScript 是一种动态语言,所以它允许我们随时对现存对象的属性和方法进行修改。其中自然也包括添加与删除属性。

> var hero = {};
> typeof hero.breed;
< "undefined"
> hero.breed = 'turtle';
> hero.name = 'Leonardo';
> hero.sayName = function() {
      return hero.name;
  }
> hero.sayName();
< "Leonardo"

删除一个属性

> delete hero.name;
< true
> hero.sayName();
< "undefined"

P103

构造器函数

> function Hero() {
      this.occupation = 'Ninja';
  }
> var hero = new Hero();
> hero.occupation;
< "Ninja"

P104

全局对象
事实上,程序所在的宿主环境一般都会为其提供一个全局对象,而所谓的全局变量其实都只不过是该对象的属性罢了。

> var a = 1;
> window.a;
< 1
> this.a;
< 1

P106

构造器属性
当我们创建对象时,实际上同时也赋予了该对象一种特殊的属性 — 即构造器属性(constructor property)。该属性实际上是一个指向用于创建该对象的构造器函数的引用。

回到103页

> hero.constructor;
< function Hero() {
      this.occupation = 'Ninja';
  }

简单来说,构造器属性是默认的属性,该属性指向构造函数。

P107

instanceof 操作符
用于测试一个对象的类型,弥补了 typeof 的不足。

P108

构造器函数默认返回的是 this 对象

function C() {
    // var this = {}; //pseudo code, you can't do this
    this.a = 1;
    // return this;
}

P109

传递对象
当我们拷贝某个对象或者将它传递给某个函数时,往往传递的都是该对象的引用。因此我们在引用上所做的任何改动,实际上都会影响它所引用的原对象。

> var original = {howmany: 100};
> var nullify = function(o) {o.howmany = 0;}
> nullify(original);
> original.howmany;
< 0

P117

Function
之前,我们已经了解了函数是一种特殊的数据类型,但事实还远不止如此,它实际上是一种对象。函数对象的内建构造器是 Function(),你可以将它作为创建函数的一种备选方式(但我们并不推荐这种方式)。

> function sum(a, b) { // function declaration
      return a + b;
  }
> sum(1, 2)
< 3
> var sum = new Function('a', 'b', 'return a + b;');
> sum(1, 2)
< 3

P120

prototype 属性

> var ninja = {
      name: 'Ninja',
      say: function() {
          return 'I am a ' + this.name;
      }
  };
> function F() {};
> typeof F.prototype;
< "object"

如果我们现在对该 prototype 属性进行修改,就会发生一些有趣的变化:当前默认的空对象被直接替换成了其他对象。

> F.prototype = ninja;
> var baby_ninja = new F();
> baby_ninja.name;
< "Ninja"
> baby_ninja.say();
< "I am a Ninja"

P121

call() 与 apply()
在 JavaScript 中,每个函数都有 call()和 apply()两个方法,您可以用它们来触发函数,并指定相关的调用参数。

> var some_obj = {
      name: 'Ninja',
      say: function(who) {
          return 'Haya ' + who + ', I am a ' + this.name;
      }
  };
> some_obj.say('Dude');
< "Haya Dude, I am a Ninja"

下面,我们再创建一个 my_obj 对象,它只有一个 name 属性:

> var my_obj = {name: 'Scripting guru'};

显然,some_obj 的 say()方法也适用于 my_obj,因此我们希望将该方法当做 my_obj 自身的方法来调用。在这种情况下,我们就可以试试 say()函数中的对象方法 call():

> some_obj.say.call(my_obj, 'Dude');
> "Haya Dude, I am a Scripting guru"

实际上是转移了 this 对象

P122

arguments 实际上是一个类数组对象,它没有数组的 sort()、slice()方法,我们可以用 call 方法让它使用数组的方法。

> function f(){
      var args = [].slice.call(arguments);
      return args.reverse();
  }
> f(1,2,3,4);
< [4,3,2,1]

P154

js 中函数既可以作为普通函数使用又可以作为构造器来创建对象。相比于 Java 严谨的语法,JavaScript 的语法显得有点乱。

P156

使用原型添加方法或属性

function Gadget(name, color) {
    this.name = name;
    this.color = color;
    this.whatAreYou = function() {
        return 'I am a ' + this.color + ' ' + this.name;
    };
}
        
Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;
Gadget.prototype.getInfo = function() {
    return 'Rating: ' + this.rating + ', price: ' + this.price;
};
        
Gadget.prototype.get = function(what) { // getter
    return this[what];
};

Gadget.prototype.set = function(key, value) { // setter
    this[key] = value;
};

P164

__proto__与 prototype 并不是等价的。__proto__实际上是某个实例对象的属性,而 prototype 则是属于构造器函数的属性。
参考__proto__ 和 prototype 到底有啥区别

P165

PHP中有一个叫做in_array()的函数,主要用于查询数组中是否存在某个特定的值。JavaScript 中则没有一个叫做 inArray()的方法(不过在 ES5 中有 indexOf()方法),因此,下面我们通过 Array.prototype 来实现一个。

> Array.prototype.inArray = function(needle) {
      for (var i = 0; i < this.length; i++) {
          if (this[i] === needle) {
              return true;
          }
      }
      return false;
  }
> var colors = ['red', 'green', 'blue'];
> colors.inArray('red');
< true
> colors.inArray('yellow');
< false

字符串反转函数

> String.prototype.reverse = function() {
      return Array.prototype.reverse.apply(this.split('')).join('');
  }
> "bumblebee".reverse();
< "eebelbmub"

P166

关于扩展内建对象
虽说通过原型来扩展内建对象功能强大,但是我们使用的时候得慎重考虑。我们扩展过的函数没准将来出现在内置方法中,这样很可能导致无法预期的错误。

扩展内置对象一般是向下兼容,当您用自定义方法扩展原型时,首先应该检查该方法是否已经存在。这样一来,当浏览器内存在同名内建方法时,我们可以直接调用原生方法,这就避免了方法覆盖。

if (typeof String.prototype.trim !== 'function') {
    String.prototype.trim = function () {
        return this.replace(/^\s+|\s+&/g, '' );
    };
}
> " hello ".trim();
< "hello"

P167

原型陷阱
接下来是终极无敌绕的代码环节

> function Dog() {
      this.tail = true;
  }
> var benji = new Dog();
> var rusty = new Dog();

即便在 benji 和 rusty 对象创建之后,我们也依然能为 Dog() 的原型对象添加属性,并且在属性被添加之前就已经存在的对象也可以随时访问这些新属性。现在,让我们放一个 say() 方法进去:

> Dog.prototype.say = function(){
      return 'Woof!';
  };
> benji.say();
< "Woof!"
> rusty.say();
< "Woof!"

现在,我们用一个自定义的新对象完全覆盖掉原有的原型对象:

> Dog.prototype = {
      paws: 4,
      hair: true
  };

然后 benji 和 rusty 并不能访问到新的原型对象中的属性。

补充

这里再来回顾一下 constructor 是什么,constructor 是一个属性,这个属性指向构造函数。benji 的构造函数是 Dog() ,Dog 的构造函数是 Function()。

> benji.constructor;
< Dog() {
      this.tail = true;
  }
> Dog.constructor;
< Function() { [native code] }

__proto__与 prototype 之间的区别
1.对象有属性 __proto__,指向该对象的构造函数的原型对象。
2.方法除了有属性 __proto__,还有属性 prototype,prototype 指向该方法的原型对象。

> benji.prototype
< undefined
> Dog.prototype
< {say: ƒ, constructor: ƒ}
      say: ƒ ()
      constructor: ƒ Dog()
      __proto__: Object
> benji.__proto__
< {say: ƒ, constructor: ƒ}
      say: ƒ ()
      constructor: ƒ Dog()
      __proto__: Object
> Dog.__proto__
< ƒ () { [native code] }
> benji.__proto__ === Dog.prototype
< true
> Dog.__proto__ === Function.prototype
< true

原型对象和原型的区别:prototype 并不能获取到原型,应该用 __proto__,或 Object.getPrototypeOf() 来获取。prototype只是函数的一个特殊属性,它指向了new 这个函数创造出来的对象的原型对象,但并不是原型,这里很容易混淆。

P176

将共享属性迁移到原型中去

function Shape() {}
Shape.prototype.name = 'Shape';

这样一来,当我们再用 new Shape() 新建对象时,name 属性就不再是新对象的私有属性了,而是被添加进了该对象的原型中。

P177

我们也可以通过 hasOwnProperty() 方法来明确对象自身属性与其原型链属性的区别。

P180

接下来又是一大串的代码,看得我头疼,这和我高中时候做数学题的感受一模一样。

function Shape() {}
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function() {
    return this.name;
};

function TwoDShape() {}
var F = function() {};
F.prototype = Shape.prototype;
TwoDShape.prototype = new F();
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.prototype.name = '2D shape';

function Triangle(side, height) {
    this.side = side;
    this.height = height;
}
var F = function() {};
F.prototype = TwoDShape.prototype;
Triangle.prototype = new F();
Triangle.prototype.constructor = Triangle;
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function() {
    return this.side * this.height / 2;
}

下面来测试一下

> var my = new Triangle(5, 10);
> my.getArea();
< 25
> my.toString();
< "Triangle"

通过这种方法,我们就可以保持住原型链:

> my.__proto__ === Triangle.prototype;
< true
> my.__proto__.constructor === Triangle;
< true
> my.__proto__.__proto__ === TwoDShape.prototype;
< true
> my.__proto__.__proto__.__proto__.constructor === Shape;
< true

搞得这么麻烦是为了在改变子对象属性的时候不影响父对象。

P183

将继承部分封装成函数

function extend(Child, Parent) {
    var F = function() {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
}

下面用一个完整的实例来检验一下

function extend(Child, Parent) {
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
}

function Shape() {};
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function() {
    return this.constructor.uber
        ? this.constructor.uber.toString() + ', ' + this.name
        : this.name;
}

function TwoDShape() {};
extend(TwoDShape, Shape);
TwoDShape.prototype.name = '2D shape';

function Triangle(side, height) {
    this.side = side;
    this.height = height;
}
extend(Triangle, TwoDShape);
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function() {
    return this.side * this.height / 2;
}

测试

> new Triangle().toString();
< "Shape, 2D shape, Triangle"

P185

属性拷贝

function extend2(Child, Parent) {
    var p = Parent.prototype;
    var c = Child.prototype;
    for(var i in p) {
        c[i] = p[i];
    }
    c.uber = p;
}

与之前的方法相比,这个方法在效率上略逊一筹。因为这里执行的是子对象原型的逐一拷贝,而非简单的原型链查询。所以我们必须要记住,这种方式仅适用于只包含基本数据类型的对象,所有的对象类型(包括函数与数组)都是不可复制的,因为它们只支持引用传递。

小结

学到这里我对 JavaScript 原型的理解又有了新的深度,现在我总结一下原型这个概念:
1.首先函数既可以当做普通函数来使用,又可以作为构造器。
2.作为构造器的函数只需要使用 new 关键字就能够创建一个对象。
3.构造函数和对象之间的区别就是有没有 prototype 这个属性。
4.prototype 属性指向的是一个对象。
5.用一张图来描述原型,先上代码

function Person() {}

Person.prototype.name = 'lemon';
Person.prototype.age = 20;
Person.prototype.sayName = function() {
    return this.name;
};

var person = new Person();
原型.png

P190

之前是在原型对象之间构建继承关系,现在抛开原型对象,直接在对象之间构建继承关系。

function extendCopy(p) {
    var c = {};
    for(var i in p) {
        c[i] = p[i];
    }
    c.uber = p;
    return c;
}

var shape = {
    name: 'Shape',
    toString: function() {
        return this.name;
    }
}

var twoDee = extendCopy(shape);
twoDee.name = '2D shape';
twoDee.toString = function() {
    return this.uber.toString() + ', ' + this.name;
};

var triangle = extendCopy(twoDee);
triangle.name = 'Triangle';
triangle.getArea = function() {
    return this.side * this.height / 2;
};

测试

> triangle.side = 5;
> triangle.height = 10;
> triangle.getArea();
< 25
> triangle.toString();
< "Shape, 2D shape, Triangle"

P192

深拷贝
之前的方法在拷贝对象的时候拷贝的是对象的引用,这样造成的结果是父对象和子对象指向同一个对象。深拷贝的原理是拷贝对象时新创建一个空对象,再将对象中的属性一一拷贝到空对象中。

function deepCopy(p, c) {
    c = c || {};
    for (var i in p) {
        if (p.hasOwnProperty(i)) {
            if (typeof p[i] === 'object') {
                c[i] = Array.isArray(p[i]) ? [] : {};
                deepCopy(p[i], c[i]);
            } else {
                c[i] = p[i];
            }
        }
    }
    return c;
}

现在来测试一下

> var parent = {
      numbers: [1, 2, 3],
      letters: ['a', 'b', 'c'],
      obj: {
          prop: 1
      },
      bool: true
  };
> var mydeep = deepCopy(parent);
> mydeep.numbers.push(4,5,6);
> mydeep.numbers;
< [1, 2, 3, 4, 5, 6]
> parent.numbers;
< [1, 2, 3]

ES5 标准中实现了 Array.isArray() 函数,为了支持低版本环境,我们需要自己实现一个 isArray() 方法。

if (typeof Array.isArray !== 'function') {
    Array.isArray = function(candidate) {
        return Object.prototype.toString.call(candidate) === '[object Array]';
    };
}

P200

构造器借用
继承实现的一种手段,原理是子对象构造器可以通过 call() 或 apply() 方法来调用父对象的构造器。废话不多说,直接上代码

先创建一个父类构造器 Shape()

function Shape(id) {
    this.id = id;
}
Shape.prototype.name = 'shape';
Shape.prototype.toString = function() {
    return this.name;
};

现在我们来定义 Triangle()构造器,在其中通过 apply()方法来调用 Shape() 构造器,并将相关的 this 值(即 new Triangle() 所创建的示例)和其他一些参数传递该方法。

function Triangle() {
    Shape.apply(this, arguments);
}
Triangle.prototype.name = 'Triangle';

下面,我们来测试一下,先新建一个 triangle 对象:

> var t = new Triangle(101);
> t.name;
< "Triangle"

在这里,新的 triangle 对象继承了其父对象的 id 属性,但它并没有继承父对象原型中的其他任何东西:

> t.id;
< 101
> t.toString();
< "[object Object]"

之所以 triangle 对象中不包含 Shape 的原型属性,是因为我们从来没有调用 newShape() 创建任何一个实例,自然其原型也从来没有被用到。

P228

setTimeout、setInterval
请注意,虽然我们有时意图让某个函数在数毫秒后即执行,但 JavaScript 并不保证该函数能恰好在那个时候被执行。浏览器会维护维护一个执行队列。100 毫秒的计时器只是意味着在 100 毫秒后将指定代码放入执行队列,但如果队列中仍有还在执行的代码,那么刚刚放入的代码就要等待直到它们执行结束,从而虽然我们设定了 100 毫秒的代码执行延迟时间,这段代码很可能到 120 毫秒以后才会被执行。

P237

检查元素是否存在属性
> bd.childNodes[1].hasAttributes();
< true

查看元素属性个数
> bd.childNodes[1].attributes.length;
< 1

获取属性名
> bd.childNodes[1].attributes[0].nodeName;
< "class"

获取属性值
> bd.childNodes[1].attributes[0].nodeValue;
< "opener"
> bd.childNodes[1].attributes['class'].nodeValue;
< "opener"
> bd.childNodes[1].getAttribute('class');
< "opener"

P242

遍历DOM

function walkDOM(n) {
    do {
        console.log(n);
        if (n.hasChildNodes()) {
            walkDOM(n.firstChild)
        }
    } while (n = n.nextSibling);
}

P253

document.referrer 中记录的是我们之前所访问过的页面 URL,它通常用于防盗链。
document.domain 在跨域的时候需要用到。

P255

addEventListener() 方法为元素绑定监听器。

P257

捕捉法与冒泡法
事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。

事件冒泡
事件会从最内层的元素开始发生,然后逐级往上传播,最后传播到 document。

事件捕获
与事件冒泡相反,事件会从最外层的 document 开始发生,直到最具体的元素。

参考文章 浅谈事件冒泡与事件捕获

P259

阻止事件冒泡
首先要定义一个以事件对象为参数的函数,并在函数内对该对象调用 stopPropagation() 方法

function paraHandler(e) {
    alert('clicked paragraph');
    e.stopPropagation();
}

P260

防止默认行为
在浏览器模型中,有些事件自身就存在一些预定义行为。例如,单击链接会载入另一个页面。对此,我们可以为该链接设置监听器,并使用 preventDefault() 方法禁用其默认行为。

var all_links = document.getElementsByTagName('a');
for (var i = 0; i < all_links.length; i++) {
    all_links[i].addEventListener(
        'click',
        function(e) {
            if (!confirm('Are you sure you want to follow this link?')) {
                e.preventDefault();
            }
        },
        false
    );
}

注意:并不是所有的默认行为都能够禁止,只能说大部分的是可以禁止的。

P261

在控制台中返回被单击元素(即目标元素)的 nodeName 属性值

document.addEventListener('click', function(e) {
    console.log(e.target.nodeName);
}, false);

P267

在前面的例子中,XHR 对象都是属于全局域的,myCallback 要根据这个全局对象的存在状态来访问它的 readyState、status 和 responseText 属性。除此之外还有一种方法,可以让我们摆脱对全局对象的依赖,那就是将我们的回调函数封装到一个闭包中去。

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = (function(myxhr) {
    return function() {
        myCallback(myxhr);
    }
}) (xhr);
xhr.open('GET', 'somefile.txt', true);
xhr.send('');

P269

自己封装一个 ajax 请求方法

function request(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = (function(myxhr) {
        return function() {
            if (myxhr.readyState === 4) {
                callback(myxhr);
            }
        }
    }) (xhr);
    xhr.open('GET', url, true);
    xhr.send('');
}

P275

好书推荐
《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)是软件工程领域有关软件设计的一本书,提出和总结了对于一些常见软件设计问题的标准解决方案,称为软件设计模式。该书作者为:ErichGamma, Richard Helm, Ralph Johnson,John Vlissides,后以“四人帮”(Gang of Four,GoF)著称。

P278

异步的 JavaScript 代码载入
这种方式就是动态创建 script 节点,然后将它插入 DOM

(function() {
    var s = document.createElement('scrit');
    s.setAttribute('src', 'behaviors.js');
    document.getElementsByTagName('head')[0].appendChild(s);
} ());

命名空间
为了减少命名冲突,我们通常都会尽量减少使用全局变量的机会。但这并不能根本解决问题,更好的办法是将变量和方法定义在不同的命名空间中。这种方法的实质就是只定义一个全局变量,并将其他变量和方法定义为该变量的属性。

// global namespace
var MYAPP = MYAPP || {};
// sub-object
MYAPP.event = {};
// object together with the method declarations
MYAPP.event = {
    addListener: function(el, type, fn) {
        // .. do the thing
    },
    removeListener: function(el, type, fn) {
        // ...
    },
    getEvent: function(e) {
        // ...
    }
    // ... other methods or properties
}

Element 构造器

MYAPP.dom = {};
MYAPP.dom.Element = function(type, properties) {
    var tmp = document.createElement(type);
    for (var i in properties) {
        if (properties.hasOwnProperty(i)) {
            tmp.setAttribute(i, properties[i]);
        }
    }
    return tmp;
};

P281

初始化分支

var MYAPP = {};
MYAPP.event = {
    addListener: null,
    removeListener: null
}
if (window.addEventListener) {
    MYAPP.event.addListener = function(el, type, fn) {
        el.addEventListener(type, fn, false);
    };
    MYAPP.event.removeListener = function(el, type, fn) {
        el.removeEventListener(type, fn, false);
    };
} else if (document.attachEvent) { // IE
    MYAPP.event.addListener = function(el, type, fn) {
        el.attachEvent('on' + type, fn);
    };
    MYAPP.event.removeListener = function(el, type, fn) {
        el.detachEvent('on' + type, fn);
    };
} else { // older browsers
    MYAPP.event.addListener = function(el, type, fn) {
        el['on' + type] = fn;
    };
    MYAPP.event.removeListener = function(el, type, fn) {
        el['on' + type] = null;
    };
}

P282

惰性初始
原理就是方法重写自身

var MYAPP = {};
MYAPP.myevent = {
    addListener: function(el, type, fn) {
        if(el.addEventListener) {
            MYAPP.myevent.addListener = function(el, type, fn) {
                el.addEventListener(type, fn, false);
            };
        } else if (el.attachEvent) {
            MYAPP.myevent.addListener = function(el, type, fn) {
                el.attachEvent('on' + type, fn);
            };
        } else {
            MYAPP.myevent.addListener = function(el, type, fn) {
                el['on' + type] = fn;
            };
        }
        MYAPP.myevent.addListener(el, type, fn);
    }
};

与前面初始化分支的区别:
前一个例子在页面加载的时候就初始化了。
惰性出事仅调用的时候才会初始化。

P283

如果参数列表过长,不妨试试改用对象
当一个函数的参数多于三个时,使用起来就多少会有些不太方便,因为我们不太容易记住这些参数的顺序。但我们可以用对象来代替多个参数。也就是说,让这些参数都成为某一个对象的属性。这在面对一些配置型参数时会显得尤为适合,因为它们中往往存在多个缺省参数。

MYAPP.dom.FancyButton = function(text, conf) {
    var type = conf.type || 'submit';
    var font = conf.font || 'Verdana';
    
    var b = document.createElement('input');
    b.value = text;
    b.type = type;
    b.font = font;
    return b;
};

P285

私有属性和方法

var MYAPP = {};
MYAPP.dom = {};
MYAPP.dom.FancyButton = function(text, conf) {
    var styles = {
        font: 'Verdana',
        border: '1px solid black',
        color: 'black',
        background: 'grey'
    };
    
    function setStyles(b) {
        var i;
        for (i in styles) {
            if (styles.hasOwnProperty(i)) {
                b.style[i] = conf[i] || styles[i];
            }
        }
    }
    
    conf = conf || {};
    var b = document.createElement('input');
    b.type = conf.type || 'submit';
    b.value = text;
    setStyles(b);
    return b;
};

P286

私有函数的公有化
函数对外提供 get 和 set 方法

var MYAPP = {};
MYAPP.dom = (function() {
    var _setStyle = function(el, prop, value) {
        console.log('setStyle');
    };
    var _getStyle = function(el, prop) {
        console.log('getStyle');
    };
    return {
        setStyle: _setStyle,
        getStyle: _getStyle,
        yetAnother: _setStyle
    };
} ());

P288

模块
不是很懂,倒是在 Vue 里面看多过 export 和 import

P289

链式调用
通过链式调用模式,我们可以在单行代码中一次性调用多个方法,就好像它们被链接在了一起。当我们需要连续调用若干个彼此相关的方法时,会带来很大的方便。实际上,我们就是通过前一个方法的结果(即返回对象)来调用下一个方法的,因此不需要中间变量。

var obj = new MYAPP.dom.Element('span');
obj.setText('hello');
obj.setStyle('color', 'red');
obj.setStyle('font', 'Verdana');
document.body.appendChild(obj);

我们已经知道,构造器返回的是新建对象的 this 指针。同样的,我们也可以让 setText()setStyle() 方法返回 this,这样,我们就可以直接用这些方法所返回的实例来调用其他方法,这就是所谓的链式调用:

var obj = new MYAPP.dom.Element('span');
obj.setText('hello')
    .setStyle('color', 'red');
    .setStyle('font', 'Verdana');
document.body.appendChild(obj);

P294

单例模式
书上写的是单件模式,这里应该是翻译者用词不当,所以我将它改成了单例模式。

function Logger() {
    if (!Logger.single_instance) {
        Logger.single_instance = this;
    }
    return Logger.single_instance;
}

这串代码能够保证无论 new 多少次都只有一个实例对象。
缺陷:它的唯一缺陷是 Logger 构造器的属性是公有的,因此它随时有可能会被覆盖

工厂模式
除了用关键字 new 创建对象以外,还可以用工厂模式创建对象

var MYAPP = {};
MYAPP.dom = {};
MYAPP.dom.Text = function(url) {
    this.url = url;
    this.insert = function(where) {
        var txt = document.createTextNode(this.url);
        where.appendChild(txt);
    };
};

MYAPP.dom.Link = function(url) {
    this.url = url;
    this.insert = function(where) {
        var link = document.createElement('a');
        link.href = this.url;
        link.appendChild(document.createTextNode(this.url));
        where.appendChild(link);
    };
};

MYAPP.dom.Image = function(url) {
    this.url = url;
    this.insert = function(where) {
        var im = document.createElement('img');
        im.src = this.url;
        where.appendChild(im);
    };
};

给 MYAPP.dom 工具添加一个工厂方法

MYAPP.dom.factory = function(type, url) {
    return new MYAPP.dom[type](url);
};

调用

var url = 'http://www.phpied.com/images/covers/oojs.jpg';
var image = MYAPP.dom.factory('Image', url);
image.insert(document.body);

P297

装饰器模式
作用是拓展对象功能

var tree = {};
tree.decorate = function() {
    alert('Make sure the tree won\'t fall');
};

tree.RedBalls = function() {
    this.decorate = function() {
        this.RedBalls.prototype.decorate();
        alert('Put on some red balls');
    };
};

tree.BlueBalls = function() {
    this.decorate = function() {
        this.BlueBalls.prototype.decorate();
        alert('Add blue balls');
    };
};

tree.Angel = function() {
    this.decorate = function() {
        this.Angel.prototype.decorate();
        alert('An angel on the top');
    };
};

添加装饰器

tree.getDecorator = function(deco) { //装饰器
    tree[deco].prototype = this;
    return new tree[deco];
};

使用

tree = tree.getDecorator('BlueBalls');
tree = tree.getDecorator('Angel');
tree = tree.getDecorator('RedBalls');

用文字来解释上面3串代码就是将 tree 设置为 tree.BlueBalls 的原型对象并且返回 tree.BlueBalls 对象,再将 tree.BlueBalls 设置为 tree.Angel 的原型对象,最后将 tree.Angel 设置为 tree.RedBalls 的原型对象。

P299

观察者模式
当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。接下来是模板代码:

var observer = {
    addSubscriber: function(callback) {
        if (typeof callback === 'function') {
            this.subscribers[this.subscribers.length] = callback;
        }
    },
    removeSubscriber: function(callback) {
        for (var i = 0; i < this.subscribers.length; i++) {
            if (this.subscribers[i] === callback) {
                delete this.subscribers[i];
            }
        }
    },
    publish: function(what) {
        for (var i = 0; i < this.subscribers.length; i++) {
            if (typeof this.subscribers[i] === 'function') {
                this.subscribers[i](what);
            }
        }
    },
    make: function(o) {
        for (var i in this) {
            if (this.hasOwnProperty(i)) {
                o[i] = this[i];
                o.subscribers = [];
            }
        }
    }
};

这串代码有三个重要且必定包含的方法:
1.将函数添加进入栈的 addSubscriber() 方法
2.将函数移除栈的 removeSubscriber() 的方法
3.执行栈中所有函数的 publish() 方法

另外我发现观察者模式除了能够监听对象状态变化,还能够扩展函数功能。将不同函数 push 到同一个任务栈中,这样就能够实现函数功能增强。而且这样还有一个好处就是把功能模块化(好像叫做切片处理),每一个模块既能独立存在又能整合在一起。

相关文章

  • 《JavaScript面向对象编程指南》笔记

    P8 类JavaScript 与 C++或 Java 这种传统的面向对象语言不同,它实际上压根儿没有类。该语言的一...

  • 2017上半年目标

    1、学习书单:《javascript面向对象编程指南》《你不知道的javascript》《正则指引》《深入Reac...

  • 构造函数与 new 命令

    JavaScript 语言具有很强的面向对象编程能力,本章介绍 JavaScript 如何进行面向对象编程。 对象...

  • Javascript面向对象编程

    阮一峰文档备忘 Javascript 面向对象编程(一):介绍封装 Javascript 面向对象编程(二):介绍...

  • JS创建对象方案(一)

    5.1 JavaScript的面向对象 JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程:...

  • JavaScript学习笔记(一)

    Javascript面向对象 1. 面向对象编程介绍 1.1 两大编程思想 面向过程 & 面向对象 1.2 面向过...

  • JavaScript 面向对象编程

    学习笔记,非原创。谢谢 JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样...

  • JavaScript学习

    javascript面向对象 初学javascript,感觉javascript的面向对象编程还是很有意思的,在此...

  • javascript的面向对象

    javascript面向对象 初学javascript,感觉javascript的面向对象编程还是很有意思的,在此...

  • ajax

    1. 面向对象 javascript 具有面向过程,面向对象,函数式编程的特点 javascript 重要 原型/...

网友评论

      本文标题:《JavaScript面向对象编程指南》笔记

      本文链接:https://www.haomeiwen.com/subject/qeoiqqtx.html