《JavaScript權威指南第六版》學習筆記-物件

xf27發表於2017-10-28

第六章、物件

  • 物件是JavaScript的基本資料型別。

  • 物件是一種複合值:它將很多值(原始值或者其他物件)聚合在一起,可以通過名字訪問這些值。

  • 物件也可看做是屬性的無序集合,每個屬性都是一個名/值對。

  • 屬性名是字串,因此我們可以把物件看成從字串到值的對映。

  • "雜湊"(hash),"雜湊表"(hashtable),"字典"(dictionary),"關聯陣列"(accociative array).

  • JavaScript物件還可以從一個稱為原型的物件繼承屬性。

  • 物件的方法通常是繼承的屬性。這種"原型式繼承"(prototypal inheritance)是JavaScript的核心特徵。

  • JavaScript物件是動態的——可以新增屬性也可以刪除屬性——但它們常用來模擬靜態物件以及靜態型別語言中的"結構體"(struct).

  • 除了字串,數字,true,false,null和undefined之外,JavaScript中的值都是物件。

  • 物件是可變的,我們通過引用而非值來操作物件。

  • 物件最常見的用法是建立(create),設定(set),查詢(query),刪除(delete),檢測(test)和列舉(enumerate)

  • 屬性包括名字和值。

屬性名可以是包含空字串在內的任意字串,但物件中不能存在兩個同名的屬性。

值可以是任意JavaScript值,或者可以是一個getter或setter函式

建立物件

可以通過物件直接量,關鍵字new和Object.create()函式來建立物件。

1.物件直接量

物件直接量是一個表示式,這個表示式的每次運算都建立並初始化一個新的物件。

var empty = {};           //沒有任何屬性的物件
var point = {x:0,y:0};       //兩個屬性
var point2 = {x:point.x,y:ponit.y+1};     //更復雜的值
var book = {
  "main title":"JavaScript",         //屬性名字裡有空格,必須使用字串表示
  "sub-title":"The Definitive Guide",       //屬性名字裡有連字元,必須使用字串表示
  "for":"all audiences",          //"for"是保留字,因此必須使用引號
  author:{
    firstname:"David",       //這個屬性的值是一個物件
    surname:"Flanagan"         //注意,這裡的屬性名都沒有引號
  }
}複製程式碼

2.通過new建立物件

  • new運算子建立並初始化一個新物件。

  • 關鍵字new後跟隨一個函式呼叫。

  • 這裡的函式稱做建構函式(constructor),建構函式用以初始化一個新建立的物件。

內建建構函式

var o = new Object();    //建立一個空物件,和{}一樣
var d = new Date();      //建立一個表示當前時間的Date函式
var r = new RegExp("js");      //建立一個可以進行模式匹配的RegExp物件複製程式碼

3.原型

  • 每一個JavaScript物件(null除外)都和另一個物件相關聯。

  • "另一個"物件就是我們熟知的原型,每一個物件都從原型繼承屬性。

  • 所有通過物件直接量建立的物件都具有同一個原型物件,並可以通過JavaScript程式碼Object.prototype獲得對原型物件的引用。

  • 沒有原型的物件為數不多,Object.prototype就是其中之一。它不繼承任何屬性。其他原型物件都是普通物件,普通物件都具有原型。

  • 所有的內建建構函式(以及大部分自定義的建構函式)都具有一個繼承自Object.prototype的原型。例如,Date.prototype的屬性繼承自Object.prototype,因此由new Date()建立的Date物件的屬性同時繼承自Date.prototype和Object.prototype.這一系列連結的原型物件就是所謂的"原型鏈"(prototype chain),

4.Object.create()

  • 它建立一個新物件,其中第一個引數是這個物件的原型。

  • Object.create()是一個靜態函式,而不是提供給某個物件呼叫的方法。

  • 使用它的方法很簡單,只須傳人所需的原型物件

eg:

var o1 = Object.create({x:1,y:2});     //o1繼承了屬性x和y複製程式碼
  • 可以通過傳人蔘數null來建立一個沒有原型的新物件

eg:

var o2 = Object.create(null);         //o2不繼承任何屬性和方法複製程式碼
  • 如果想建立一個普通的空物件

eg:

var o3 = Object.create(Object.prototype);    //o3和{}和new Object{}一樣複製程式碼
  • 可以通過任意原型建立新物件

eg:通過原型繼承建立一個新物件

//inherit()返回一個繼承自原型物件p的屬性的新物件
//這裡使用ECMAScript5中的Object.create()函式(如果存在的話)
//如果不存在Object.create(),則退化使用其他方法
function inherit(p){
  if(p == null){
    throw TypeError();      //p是一個物件,但不能是null
  }
  if(Object.create){         //如果Object.create()存在
    return Object.create(p);          //直接使用它
  }
  var t = typeof p;        //否則進行進一步檢測
  if(t !== "object" && t !== "function"){
      throw TypeError();   
  }
  function f(){};      //定義一個空建構函式
  f.prototype = p;         //將其原型屬性設定為p
  return new f();         //使用f()建立p的繼承物件
}     複製程式碼

當函式讀取繼承物件的屬性時,實際上讀取的是繼承來的值。如果給繼承物件的屬性賦值,則這些屬性只會影響這個繼承物件自身,而不是原始物件

var o = {x:'don't change this value'};
library_function(inherit(o));   //防止對o的意外修改複製程式碼
屬性的查詢和設定
  • 可以通過點(.)或方括號([])運算子來獲取屬性的值。

eg:

var author = book.author;  //得到book的"author"屬性
var name = author.surname;    //得到author的"surname"屬性
var title = book["main title"];  //得到book的"main title"屬性複製程式碼
  • 通過點和方括號也可以建立屬性或給屬性賦值,但需要將它們放在賦值表示式的左側

eg:

book.edition = 6;   //給book建立一個名為"edition"的屬性
book["main title"] = "ECMAScript";   //給"main title"屬性賦值複製程式碼

1.作為關聯陣列的物件

object.property   //使用點運算子和一個識別符號
object["property"]       
//第二種語法使用方括號和一個字串,看起來更像陣列,只是這個陣列元素是通過字串索引而不是數字索引。這種陣列就是我們所說的關聯陣列(associative array),也稱做雜湊、對映或字典(dictionary)複製程式碼
  • 當通過[]來訪問物件的屬性時,屬性名通過字串來表示。字串是JavaScript的資料型別,在程式執行時可以修改和建立它們。

eg:

var addr = "";
for(var i = 0;i < 4;i++){
  addr += customer["address" + i] + '\n';
}複製程式碼

使用陣列寫法和用字串表示式來訪問物件屬性的靈活性。

function addstock(portfolio,stockname,shares){
  portfolio[stockname] = shares;
}複製程式碼
  • 當使用for/in迴圈遍歷關聯陣列時,就可以清晰地體會到for/in的強大之處。

eg:

function getvalue(portfolio){
  var total = 0.0;
  for(stock in portfolio){           //遍歷portfolio中的每隻股票
    var shares = portfolio[stock];  //得到每隻股票的份額;
    var price = getquote(stock);        //查詢股票價格
    total += shares * price;       //將結果累加至total中
  }
  return total;      //返回total的值
}複製程式碼

2.繼承

  • JavaScript物件具有"自有屬性"(own property),也有一些屬性是從原型物件繼承而來的。

eg:

var o = {}  //o 從Object.prototype繼承物件的方法
o.x = 1;   //給o定義一個屬性x
var p = inherit(o);   //p繼承o和Object.prototype
p.y = 2;  //給p定義一個屬性y
var q = inherit(p);   //q繼承p,o和Object.prototype
var s = q.toString();   //toString繼承自Object.prototype
q.x + q.y  //=>3:x和y分部繼承自o和p複製程式碼

現在假設給物件o的屬性x賦值,如果o中已經有屬性x(這個屬性不是繼承來的),那麼這個賦值操作只改變這個已有屬性x的值。如果o中不存在屬性x,那麼賦值操作給o新增一個新屬性x。如果之前o繼承自屬性x,那麼這個繼承的屬性就被新建立的同名屬性覆蓋了。

  • 屬性賦值操作首先檢查原型鏈,以此判斷是否允許賦值操作。

  • 在JavaScript中,只有在查詢屬性時才會體會到繼承的存在,而設定屬性則和繼承無關

eg:

var unitcircle = {r:1};   //一個用來繼承的物件
var c = inherit(unitcircle);    //c繼承屬性r
c.x = 1; c.y =1;    //c定義兩個屬性
c.r = 2;   //c覆蓋繼承來的屬性
unitcircle.r;   //=>1,原型物件沒有修改複製程式碼

屬性賦值要麼失敗,要麼建立一個屬性,要麼在原始物件中設定屬性

3.屬性訪問錯誤

  • 屬性訪問並不總是返回或設定一個值。

  • 查詢一個不存在的屬性並不會報錯,返回undefined

eg:

book.subtitle;   //=>undefined:屬性不存在複製程式碼
  • 如果物件不存在,那麼試圖查詢這個不存在的物件的屬性就會報錯。

eg:

//丟擲一個型別錯誤異常,undefined沒有length屬性
var len = book.subtitle.length;複製程式碼

eg:避免出錯的方法

//一種冗餘但很易懂的方法
var len = undefined;
if(book){
  if(boo.subtitle){
    len = book.subtitle.length;
  }
}   

//一種更簡練的常用方法,獲取subtitle的length屬性或undefined
var len = book && book.subtitle && book.subtitle.length複製程式碼
刪除屬性
  • delete運算子可以刪除物件的屬性。

eg:

delete book.author;   //book不再有屬性author
delete book["main title"];  //book也不再有屬性"main title"複製程式碼
  • delete運算子只能刪除自有屬性,不能刪除繼承屬性(要刪除繼承屬性必須從定義這個屬性的原型物件上刪除它,而且這會影響到所有繼承自這個原型的物件)。

eg:

var a = {p:{x:1}};
b = a.p;
delete a.p;   //=>b.x=1複製程式碼
  • 當delete表示式刪除成功或沒有任何副作用(比如刪除不存在的屬性)時,他返回true。如果delete後不是一個屬性訪問表示式,delete同樣返回true

eg:

var o = {x:1};  //o有一個屬性x,並繼承屬性toString
delete o.x;    //刪除x,返回true
delete o.x;   //什麼都沒做(x已經不存在了),返回true
delete o.toString;   //什麼也沒做(toString是繼承來的),返回true
delete 1;  //無意義,返回true複製程式碼
  • delete不能刪除那些可配置性為false的屬性

eg:

delete Object.prototype;        //不能刪除,屬性是不可配置的
var x = 1;  //宣告一個全域性變數
delete this.x;  //不能刪除這個屬性
function f(){};   //宣告一個全域性函式
delete this.f;     //也不能刪除全域性函式複製程式碼
  • 當在非嚴格模式中刪除全域性物件的可配置屬性時

eg:

this.x = 1; //建立一個可配置的全域性屬性(沒有用var)
delete x;   //將它刪除複製程式碼
檢測屬性

檢測集合中成員的所屬關係——判斷某個屬性是否存在於某個物件中。可以通過in運算子,hasOwnPreperty()和propertyIsEnumerable()方法來完成這個工作,甚至僅通過屬性查詢也可以做到這一點。

  • in運算子的左側是屬性名(字串),右側是物件。如果物件的自有屬性或繼承屬性中包含這個屬性則返回true

eg:

var o ={x:1};
"x" in o;   //true:"x"是o的屬性
"toString" in o;  //true:o繼承toString屬性
"y" in o;  //false:'y'不是o的屬性複製程式碼
  • 物件的hasOwnProperty()方法用來檢測給定的名字是否是物件的自有屬性。對於繼承屬性它將返回false

eg:

var o = {x:1}
o.hasOwnProperty("x");   //true:o有一個自有屬性x
o.hasOwnProperty("y");   //false:o中不存在屬性y
o.hasOwnProperty("toString");  //false:toString是繼承屬性複製程式碼
  • propertyIsEnumerable()是hasOwnproperty()的增強版,只有檢測到是自有屬性且這個屬性的可列舉性(enumerable attribute)為true時它才返回true.

eg:

var o = inherit({y:2});
o.x = 1;
o.propertyIsEnumerable("x");   //true:o有一個可列舉的自有屬性x
o.propertyIsEnumerable("y");   //false:y是繼承來的
Object.prototype.propertyIsEnumerable("toString");   //false:不可列舉複製程式碼
  • 除了使用in運算子之外,另一種更簡便的方法是使用"!=="判斷一個屬性是否是undefined

eg:

var o = {x:1}
o.x !== undefined;   //true:o中有屬性x
o.y !== undefined;   //false:o中沒有屬性y
o.toString !== undefined;  //true:o繼承了toString屬性複製程式碼
  • in可以區分不存在的屬性和存在但值為undefined的屬性

eg:

var o ={x:undefined}  //屬性被顯示賦值為undefined
o.x !== undefined;   //false:屬性存在,但值為undefined
o.y !== undefined;   //false:屬性不存在
"x" in o;  //true:屬性存在
"y" in o;  //false:屬性不存在
delete o.x;   //刪除了屬性x
"x" in o;  //false:屬性不存在複製程式碼
列舉屬性

除了檢測物件的屬性是否存在,我們還會經常遍歷物件的屬性。通常使用for/in迴圈遍歷

var o ={x:1,y:2,z:3}   //三個可列舉的自有屬性
o.propertyIsEnumerable("toString")  //=>false,不可列舉
for(p in o){    //遍歷屬性
  console.log(p);    //輸出x,y,z,不會輸出toString
}     複製程式碼

過濾for/in迴圈返回的屬性。

for(p in o){
  if(!o.hasOwnProperty(p)){
      continue;      //跳過繼承屬性
  }
  for(p in o){
    if(typeof o[p] === "function"){
      continue;      //跳過方法
    }
  }
}複製程式碼

用來列舉屬性的物件工具函式

<!-- 把p中的可列舉屬性複製到o中,並返回o
如果o和p中含有同名屬性,則覆蓋o中的屬性
這個函式並不處理getter和setter以及複製屬性 -->

function extend(o,p){
  for(prop in p){   //遍歷p中的所有屬性
    o[prop] = p[prop];    //將屬性新增至o中
  }
  return o;
}

<!-- 把p中的可列舉屬性複製到o中,並返回o
如果o和p中含有同名屬性,o中的屬性將不受影響
這個函式並不處理getter和setter以及複製屬性
 -->
function merge(o,p){
  for(prop in p){       //遍歷p中是所有屬性
    if(o.hasOwnProperty[prop]){   //過濾掉已經在o中存在的屬性
      continue;
    }
    o[prop] = p[prop];      //將屬性新增至o中
  }
  return o;
}


<!-- 如果o中的屬性在p中沒有同名屬性,則從o中刪除這個屬性
返回o -->

function restrict(o,p){
  for(prop in o){  //遍歷o中的所有屬性
    if(!(prop in p)){        //如果在p中不存在,則刪除之
      delete o[prop];     
    }
  }
  return o;
}

<!-- 如果o中的屬性在p中存在同名屬性,則從o中刪除這個屬性
返回o -->

function subtract(o,p){
  for(prop in p){      //迴圈遍歷p中的所有屬性
    delete o[prop];       //從o中刪除(刪除一個不存在的屬性不會報錯)
  }
  return o;
}

<!-- 返回一個新物件,這個物件同時擁有o的屬性和p的屬性
如果o和p中有重名屬性,使用p中的屬性值 -->

function union(o,p){
  return extend(extend(extend({},o),p))
}複製程式碼

-------------此處很難理解----------------------

屬性getter和setter
  • 物件屬性是由名字、值和一組特性(attribute)構成的。

  • 在ECMAScript5中,屬性值可以用一個或兩個方法替代,這兩個方法就是getter和setter.

  • 由getter和setter定義的屬性稱做"存取器屬性"(accessor property),它不同於"資料屬性"(data property),資料屬性只有一個簡單的值。

  • 和資料屬性不同,存取器屬性不具有可寫性(writable attribute).如果屬性同時具有getter和setter方法,那麼它是一個讀/寫屬性。

eg:

var o = {
  //普通的資料屬性
  data_prop:value,

  //存取器屬性都是成對定義的函式
  get accessor_prop(){
    //這裡是函式體
  }
  set accessor_prop(value){
    //這裡是函式體
  }
}複製程式碼
  • 在函式體內的this指向表示這個點的物件

eg:

var p ={
  //x和y是普通的可讀寫的資料屬性
  x:1.0,
  y:1.0,

  //r是可讀寫的存取器屬性,它有getter和setter
  //函式體結束後不要忘記帶上逗號
  get r(){
    return Math.sqrt(this.x*this.x + this.y*this.y);
  },
  set r(newvalue){
    var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y);
    var ratio = newvalue/oldvalue;
    this.x *= ratio;
    this.y *= ratio;
  },
  //theta是隻讀存取器屬性,它只有getter方法
  get theta(){
    return Math.atan2(this.y,this.x);
  }
}複製程式碼
  • 和資料屬性一樣,存取器屬性是可以繼承的

eg:

var q  inherit(p);     //建立一個繼承getter和setter的新物件
q.x = 1,q.y = 1;        //給q新增屬性
console.log(q.r);      //可以使用繼承的存取器屬性
console.log(q.theta);

//這個物件產生嚴格自增的序列號
var serialnum = {
  //這個資料屬性包含下一個序列號
  //$符號暗示這個屬性是一個私有屬性
  $n:0,

  //返回當前值,然後自增
  get next(){
    return this.$n++;
  },

  //給n設定新的值,但只有當它比當前值大時才設定成功
  set next(n){
    if(n >= this.$n){
      this.$n = n;
    }else{
      throw "序列號的值不能比當前值小";
    }
  }
}


var random = {
  get octet(){
    return Math.floor(Math.random()*256);
  },
  get uint16(){
    return Math.floor(Math.random()*65536);
  },
  get int16(){
    return Math.floor(Math.random()*65536)-32768
  }
}複製程式碼
屬性的特性
  • 資料屬性的4個特性分別是它的值(value),可寫性(writable),可列舉性(enumerable)和可配置性(configurable)

  • 存取器屬性的4個特性是讀取(get),寫入(set),可列舉性和可配置性。

  • 通過呼叫Object.getOwnPropertyDescriptor()可以獲得某個物件特定屬性的屬性描述符,只能得到自有屬性的描述符:

eg:

返回Object {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor({x:1},"x");複製程式碼
  • 要想獲得繼承屬性的特性,需要遍歷原型鏈Object.getProtorypeOf()

  • 要想設定屬性的特性,或者想讓新建屬性具有某種特性,則需要呼叫Object.definePeoperty(),傳人要修改的物件,要建立或修改的屬性的名稱以及屬性描述符物件

eg:

var o = {};  //建立一個空物件
//新增一個不可列舉的資料屬性x,並賦值為1
Object.defineProperty(o,"x",{
  value:1,
  writable:true,
  enumerable:false,
  configurable:true
  });

//屬性是存在的,但不可列舉
o.x;    //=>1
Object.keys(o)  //=>[]

//現在對屬性x做修改,讓它變為只讀
Object.defineProperty(o,"x",{writable:false});

//試圖更改這個屬性的值
o.x = 2;     //操作失敗但不報錯,而在嚴格模式中丟擲型別錯誤異常
o.x //=>1

//屬性依然是可配置的,因此可以通過這種方式對它進行修改
Object.defineProperty(o,"x",{value:2});
o.x //=>2

//現在將x從資料屬性修改為存取器屬性
Object.defineProperty(o,"x",{get:function(){
  return o;
  }});
o.x //=>0複製程式碼
  • 如果要同時修改或建立多個屬性,則需要使用Object.defineProperties().第一個引數是要修改的物件,第二個引數是一個對映表,它包含要新建或修改的屬性的名稱,以及它們的屬性描述符

eg:

var p = Object.defineProperties({},{
  x:{value:1,writable:true,enumerable:true,configurable:true},
  y:{value:1,writable:true,enumerable:true,configurable:true},
  r:{
    get:function(){
      return Math.sqrt(this.x*this.x+this.y*this.y)
    },
    enumerable:true,
    configurable:true
  }
  });複製程式碼
  • 複製屬性的特性

eg:

/**
 * 給Object.prototype新增一個不可列舉的extend()方法
 * 這個方法繼承自呼叫它的物件,將作為引數傳人的物件的屬性一一複製
 * 除了值以外,也複製屬性的所有特性,除非在目標物件中存在同名的屬性
 * 引數物件的所有自有物件(包括不可列舉的屬性)也會一一複製
 */

  Object.defineProperty(Object.prototype,"extend",{         //定義Object.prototype.extend
    wirtable:true,
    enumerable:false,    //將其定義為不可列舉
    configurable:true,
    value:function(o){       //值就是這個函式
      //得到所有的自有屬性,包括不可列舉屬性
      var names = Object.getOwnPropertyNames(o);
      //遍歷它們
      for(var i = 0;i < names.length;i++){
        //如果屬性已經存在,則跳過
        if(names[i] in this){
          continue;
        }
        //獲得O中的屬性的描述符
        var desc = Object.getOwnPropertyDescriptor(o,names[i]);
        //用它給this建立一個屬性
        Object.defineProperty(this,names[i],desc);
      }
    }
    })複製程式碼
物件的三個屬性

每個物件都有與之相關的原型(prototype),類(class)和可擴充套件性(wxtensible attribute)

1.原型屬性

  • 物件的原型屬性是用來繼承屬性的

  • 原型屬性是在例項物件建立之初就設定好的,

A.通過物件直接量建立的物件使用Object.prototype作為它們的原型。

B.通過new建立的物件使用建構函式的prototype屬性作為它們的原型。

C.通過Object.create()建立的物件使用第一個引數(也可以是null)作為它們的原型。

  • 將物件作為引數傳人Object.getPrototypeOf()可以查詢它的原型

  • 表示式o.constructor.prototype來檢測一個物件的原型

  • 通過new表示式建立的物件,通常繼承一個constructor屬性,這個屬性指代建立這個物件的建構函式

  • 通過物件直接量Object.create()建立的物件包含一個名為constructor的屬性,這個屬性指代Object()建構函式

  • constructor.prototype是物件直接量的真正的原型

  • 檢測一個物件是否是另一個物件的原型(或處於原型鏈中),isPrototypeOf()

eg:

var p = {x:1};        //定義一個原型物件
var o = Object.create(p);    //使用這個原型建立一個物件
p.isPrototypeOf(o);  //=>true:o繼承自p
Object.prototype.isPrototypeOf(o);   //=>true:p繼承自Object.prototype複製程式碼

2.類屬性

物件的類屬性是一個字串,用以表示物件的型別資訊。

  • 預設toString()方法

返回:

[object class]複製程式碼
  • 獲得物件的類

classof()函式

function classof(o){
  if(o == null){
      return "Null";
  }
  if(o == undefined){
    return "Undefined";
  }
  return Object.prototype.toString.call(o).slice(8,-1);
}複製程式碼

alt text
alt text

3.可擴充套件性

物件的可擴充套件性用以表示是否可以給物件新增新屬性。

  • 通過將物件傳人Object.esExtensible(),來判斷該物件是否是可擴充套件的。

  • 通過呼叫Object.preventExtensions()將物件轉換為不可擴充套件的。一旦將物件轉換為不可擴充套件的,就無法再將其轉換為可擴充套件的了。

  • 可擴充套件性的目的是將物件"鎖定",以避免外界的干擾

  • Object.seal() 封閉

  • Object.freeze() 凍結

eg:

var o = Object.seal(Object.create(Object.freeze({x:1}),
                                  {y:{value:2,writable:true}}))複製程式碼
序列化物件

物件序列化(serialization)是指將物件的狀態轉換為字串,也可將字串還原為物件

  • JSON.stringify() 序列化物件

  • JSON.parse() 還原物件

  • JSON作為資料交換格式,JSON的全稱是"JavaScript Object Notation"——JavaScript物件表示法

eg:

o = {
  x:1,
  y:{
    z:[false,null,""]
  }
};           //定義一個測試物件
s = JSON.stringify(o);        //=>s:'{"x":1,"y":{"z":[false,null,""]}}'
p = JSON.parse(s);            //p是o的深拷貝複製程式碼
  • JSON的語法是JvaScript語法的子集,它並不能表示JavaScript裡的所有值。
物件方法

所有的JavaScript物件都從Object.prototype繼承屬性(除了那些不通過原型顯式建立的物件)。

1.toString()方法

var s = {x:1,y:1}.toString();     //=>"[object Object]"複製程式碼

2.toLocaleString()方法

這個方法返回一個表示這個物件的本地化字串

3.toJSON方法

JSON.stringify()方法會呼叫toJSON()方法

4.valueOf()方法

當JavaScript需要將物件轉換為某種原始值而非字串的時候才會呼叫它,尤其是轉換為數字的時候。

|版權宣告:本文為summer博主原創文章,未經博主允許不得轉載。

相關文章