《JavaScript面向對象編程指南》筆記

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 到同一個任務棧中,這樣就能夠實現函數功能增強。而且這樣還有一個好處就是把功能模塊化(好像叫做切片處理),每一個模塊既能獨立存在又能整合在一起。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容

  • 災禍- 至尊寶 作詞:陶凌霏 作曲:戴荃 編曲:關天天 日照宿舍 冬日暖暖 十一九號 消防演練 誰叫我不在宿舍 誰...
    至t尊l寶f閱讀 208評論 0 0
  • 快速聚類分析得到的結果要簡單易懂的多,且對計算機要求不是很高,因而其應用比層次聚類分析要高。實質是K-mean聚類...
    肖玉賢閱讀 841評論 0 0
  • O 臨界知識:可以用(會用)的知識才叫做知識 底層規律/知識模型:在100個以內 知識模型來自數學,科學,工程學,...
    longliveping閱讀 180評論 0 0