深入理解JS中的淺拷貝與深拷貝

一、棧內(nèi)存與堆內(nèi)存

棧內(nèi)存與堆內(nèi)存 、淺拷貝與深拷貝,可以說(shuō)是前端程序員的內(nèi)功,要知其然,知其所以然。

1、棧與棧內(nèi)存

棧的定義
  • 后進(jìn)者先出,先進(jìn)者后出,簡(jiǎn)稱(chēng) 后進(jìn)先出(LIFO),這就是典型的結(jié)構(gòu)。
  • 新添加的或待刪除的元素都保存在棧的末尾,稱(chēng)作棧頂,另一端就叫棧底。
  • 在棧里,新元素都靠近棧頂,舊元素都接近棧底。
  • 從棧的操作特性來(lái)看,是一種 操作受限 的線(xiàn)性表,只允許在一端插入和刪除數(shù)據(jù)。
  • 不包含任何元素的棧稱(chēng)為空棧。
  • 棧也被用在編程語(yǔ)言的編譯器和內(nèi)存中保存變量、方法調(diào)用等,比如函數(shù)的調(diào)用棧。
棧內(nèi)存
  • 它們存儲(chǔ)的值都有固定的大小,保存在棧內(nèi)存空間,通過(guò)按值訪(fǎng)問(wèn),并由系統(tǒng)自動(dòng)分配和自動(dòng)釋放。
  • 這樣帶來(lái)的好處就是,內(nèi)存可以及時(shí)得到回收,相對(duì)于堆來(lái)說(shuō),更加容易管理內(nèi)存空間。
  • 主要用于存儲(chǔ)各種基本數(shù)據(jù)類(lèi)型的變量,以及對(duì)象變量的指針。
  • JavaScript 中的 Boolean、Null、UndefinedNumber、String、Symbol、BigInt 都是基本數(shù)據(jù)類(lèi)型。

2、堆與堆內(nèi)存

堆的定義
  • 堆數(shù)據(jù)結(jié)構(gòu)是一種樹(shù)狀結(jié)構(gòu)。
  • 它的存取數(shù)據(jù)的方式,與書(shū)架與書(shū)非常相似。我們不關(guān)心書(shū)的放置順序是怎樣的,只需知道書(shū)的名字就可以取出我們想要的書(shū)了。
  • 好似在 JSON 格式的數(shù)據(jù)中,我們存儲(chǔ)的 key-value 是可以無(wú)序的,只要知道 key,就能取出這個(gè) key 對(duì)應(yīng)的 value。

堆與棧比較

  • 堆是動(dòng)態(tài)分配內(nèi)存,內(nèi)存大小不一,也不會(huì)自動(dòng)釋放。
  • 棧是自動(dòng)分配相對(duì)固定大小的內(nèi)存空間,并由系統(tǒng)自動(dòng)釋放。
  • 棧,線(xiàn)性結(jié)構(gòu),后進(jìn)先出,便于管理。
  • 堆,一個(gè)混沌,雜亂無(wú)章,方便存儲(chǔ)和開(kāi)辟內(nèi)存空間。
堆內(nèi)存

引用類(lèi)型(如對(duì)象、數(shù)組、函數(shù)等)是保存在堆內(nèi)存中的對(duì)象,值大小不固定,棧內(nèi)存中存放的該對(duì)象的訪(fǎng)問(wèn)地址指向堆內(nèi)存中的對(duì)象,JavaScript 不允許直接訪(fǎng)問(wèn)堆內(nèi)存中的位置,因此操作對(duì)象時(shí),實(shí)際操作對(duì)象的引用。
JavaScript 中的 Object、Array、Function、RegExp、Date 是引用類(lèi)型。

堆內(nèi)存與棧內(nèi)存比較

棧內(nèi)存 堆內(nèi)存
存儲(chǔ)基礎(chǔ)數(shù)據(jù)類(lèi)型 存儲(chǔ)引用數(shù)據(jù)類(lèi)型
按值訪(fǎng)問(wèn) 按引用訪(fǎng)問(wèn)
存儲(chǔ)的值大小固定 存儲(chǔ)的值大小不定,可動(dòng)態(tài)調(diào)整
由系統(tǒng)自動(dòng)分配內(nèi)存空間 由代碼進(jìn)行指定分配
空間小,運(yùn)行效率高 空間大,運(yùn)行效率相對(duì)較低
先進(jìn)后出,后進(jìn)先出 無(wú)序存儲(chǔ),可根據(jù)引用直接獲取
棧內(nèi)存堆內(nèi)存

棧內(nèi)存中的變量一般都是已知大小或者有范圍上限的,算作一種簡(jiǎn)單存儲(chǔ)。而堆內(nèi)存存儲(chǔ)的對(duì)象類(lèi)型數(shù)據(jù)對(duì)于大小這方面,一般是未知的。這就是為什么null作為一個(gè)object類(lèi)型的變量卻存儲(chǔ)在棧內(nèi)存中的原因吧。

當(dāng)我們定義一個(gè)const obj ={name:'king'}對(duì)象的時(shí)候,我們說(shuō)的常量obj其實(shí)是指針,就是變量obj的值不允許改變,也就是變量obj指向的堆內(nèi)存的地址不能改變,但是,該堆內(nèi)存地址內(nèi)數(shù)據(jù)本身的大小或?qū)傩允强梢愿淖兊摹?/p>

既然知道了const在內(nèi)存中的存儲(chǔ),那么constlet定義的變量不能二次定義的流程也就比較容易猜出來(lái)了,每次使用const或者let去初始化一個(gè)變量的時(shí)候,會(huì)首先遍歷當(dāng)前的內(nèi)存棧,看看有沒(méi)有重名變量,有的話(huà)就返回錯(cuò)誤。

二、數(shù)據(jù)類(lèi)型

JS中,數(shù)據(jù)分為基本數(shù)據(jù)類(lèi)型(String、NumberBoolean、NullUndefined、Symbol、BigInt)和引用數(shù)據(jù)類(lèi)型(ObjectFunction、class)。

  • 基本數(shù)據(jù)類(lèi)型是直接存儲(chǔ)在棧(Stack)內(nèi)存中的數(shù)據(jù)
  • 引用數(shù)據(jù)類(lèi)型棧內(nèi)存存儲(chǔ)的是該對(duì)象的引用地址,對(duì)象的真實(shí)數(shù)據(jù)存儲(chǔ)在堆內(nèi)存中
    引用數(shù)據(jù)類(lèi)型在棧內(nèi)存中存儲(chǔ)了引用地址(也叫指針),該引用地址或指針指向了堆內(nèi)存中實(shí)際數(shù)據(jù)的地址。


    引用數(shù)據(jù)類(lèi)型存儲(chǔ)

1、基本數(shù)據(jù)類(lèi)型的存儲(chǔ)

變量名和值都儲(chǔ)存在棧內(nèi)存中

let num = 10;
let newnum = num;

那么該num變量在棧內(nèi)存中的存儲(chǔ)如下

基本數(shù)據(jù)類(lèi)型存儲(chǔ)

變量num和值10都存儲(chǔ)在棧內(nèi)存中,num =10;讓變量num指向數(shù)字10;而newnum =num是將變量newnum存儲(chǔ)在棧內(nèi)存中,并且讓該變量指向新的值數(shù)字10。此時(shí),變量num和變量newnum沒(méi)有任何關(guān)系。注意 棧內(nèi)存的賦值都是賦值,newnum = num是指將變量num賦值給新變量newnum;而不是把變量num賦值給變量newnum。

基本數(shù)據(jù)類(lèi)型值是不可變的
let a = 1;
console.log(++a);

其實(shí)這個(gè)時(shí)候并不是變量a 指向的 1 直接加了 1,而是 新建了一個(gè) 1+1 = 2 的值,再將 a 指向這個(gè)新建出來(lái)的 2,原來(lái)的那個(gè) 1 并沒(méi)有發(fā)生改變,留由垃圾回收機(jī)制處理。也就是說(shuō)不是 a 指向的值發(fā)生了改變,而是 a 變量指針指向了一個(gè)新的值,這就是基本類(lèi)型的值是不可變的。

2、引用數(shù)據(jù)類(lèi)型的存儲(chǔ)

變量名儲(chǔ)存在棧內(nèi)存中,值儲(chǔ)存在堆內(nèi)存中,但是堆內(nèi)存中會(huì)提供一個(gè)引用地址指向堆內(nèi)存中的值,而這個(gè)地址是儲(chǔ)存在棧內(nèi)存中的。

let ary = [1, 2, 3];
let newary = ary;

那么該ary變量在內(nèi)存中的存儲(chǔ)如下

引用數(shù)據(jù)類(lèi)型存儲(chǔ)

引用數(shù)據(jù)類(lèi)型值是可變的
let obj = { name: 'king' };
obj.name = '偶爾平凡';
console.log(obj)
//{ name: '偶爾平凡' }

引用數(shù)據(jù)類(lèi)型占據(jù)空間大,大小不固定,儲(chǔ)存在堆內(nèi)存,但是指向該引用數(shù)據(jù)類(lèi)型的變量指針「a」是儲(chǔ)存在棧內(nèi)存中。 當(dāng)解釋器尋找引用值時(shí),會(huì)首先檢索其在棧中的地址,取得地址后從堆中獲得實(shí)體。

var a = new String("123");
var b = String("123");
var c = "123";
console.log(a == b, a === b, b == c, b === c, a == c, a === c);
//           true     false    true    true     true    false

我們可以看到new一個(gè)String,出來(lái)的是對(duì)象,而直接字面量賦值和工廠(chǎng)模式出來(lái)的都是字符串。

let a = new String("123");
let b = new String("123");
console.log(a == b, a === b);
console.log(null === null);
//false false
//true

我們知道,變量a 和變量b都是存儲(chǔ)在棧內(nèi)存中的,但是變量a指向一個(gè)對(duì)象,假設(shè)該對(duì)象的堆內(nèi)存地址AA00,變量b執(zhí)行一個(gè)新的對(duì)象,該新對(duì)象的堆內(nèi)存地址BB00,也就是變量a的值為AA00,變量b的值為BB00,所以變量a和變量b的值不同。而null是存儲(chǔ)在棧內(nèi)存的,所以null === nulltrue。

let a = null;
let b = null;
console.log(a === b);//true

3、基本數(shù)據(jù)類(lèi)型和引用數(shù)據(jù)類(lèi)型的銷(xiāo)毀

基本類(lèi)型在當(dāng)前執(zhí)行環(huán)境結(jié)束時(shí)銷(xiāo)毀,而引用類(lèi)型不會(huì)隨執(zhí)行環(huán)境結(jié)束而銷(xiāo)毀,只有當(dāng)所有引用它的變量不存在時(shí)這個(gè)對(duì)象才被垃圾回收機(jī)制回收。

三、淺拷貝和深拷貝的區(qū)別

基本數(shù)據(jù)類(lèi)型都是深拷貝,無(wú)需討論。引用數(shù)據(jù)類(lèi)中的淺拷貝只復(fù)制指向某個(gè)對(duì)象的指針,而不復(fù)制對(duì)象本身,新舊對(duì)象還是共享同一塊堆內(nèi)存。但深拷貝會(huì)另外創(chuàng)造一個(gè)一模一樣的對(duì)象,新對(duì)象跟原對(duì)象不共享堆內(nèi)存,修改新對(duì)象不會(huì)改到原對(duì)象。淺拷貝和深拷貝,都是圍繞堆棧內(nèi)存展開(kāi)的,一個(gè)是處理值,一個(gè)是處理指針。

四、淺拷貝的實(shí)現(xiàn)方式

1、直接賦值

let A = {
  name: "king",
  data: {
    age: 20,
  },
};
let B = {};
B = A;
B.name = "偶爾平凡";
console.log(A);
//{ name: '偶爾平凡', data: { age: 20 } }

2、Object.assign()

這是ES6中新增的對(duì)象方法,對(duì)它不了解的見(jiàn)ES6對(duì)象新增方法,它可以實(shí)現(xiàn)第一層的“深拷貝”,但無(wú)法實(shí)現(xiàn)多層的深拷貝。

第一層“深拷貝”:就是對(duì)于A(yíng)對(duì)象下所有的屬性和方法都進(jìn)行了深拷貝,但是當(dāng)A對(duì)象下的屬性如data是對(duì)象時(shí),它拷貝的是地址,也就是淺拷貝,這種拷貝方式還是屬于淺拷貝。

多層深拷貝:能將A對(duì)象下所有的屬性,及時(shí)屬性是對(duì)象,也能夠深拷貝出來(lái),讓A和B相互獨(dú)立,這種叫才叫深拷貝。

let A = {
  name: "king",
  data: {
    age: 20,
  },
};
let B = {};
Object.assign(B, A);
B.name = "偶爾平凡";
B.data.age = 15;
console.log(A);
console.log(B);
//{ name: 'king', data: { age: 15 } }
//{ name: '偶爾平凡', data: { age: 15 } }

3、擴(kuò)展運(yùn)算符

let obj = { name: "king", age: 10, se: { sex: 0 } };
let newobj = { ...obj };
newobj.name = "偶爾平凡";
newobj.se.sex = 1;
console.log(obj);
console.log(newobj);
//{ name: 'king', age: 10, se: { sex: 1 } }
//{ name: '偶爾平凡', age: 10, se: { sex: 1 } }

4、Array.prototype.concat()

let arr = [
  1,
  3,
  {
    name: "king",
  },
];
let arr2 = arr.concat();
arr2[1] = 10;
arr2[2].name = "偶爾平凡";
console.log(arr);
console.log(arr2);
//[ 1, 3, { name: '偶爾平凡' } ]
//[ 1, 10, { name: '偶爾平凡' } ]

5、Array.prototype.slice()

let arr = [
  1,
  3,
  {
    name: "king",
  },
];
let arr2 = arr.slice();
arr2[1] = 10;
arr2[2].name = "偶爾平凡";
console.log(arr);
console.log(arr2);
//[ 1, 3, { name: '偶爾平凡' } ]
//[ 1, 10, { name: '偶爾平凡' } ]

關(guān)于A(yíng)rray的slice和concat方法的補(bǔ)充說(shuō)明:Array的slice和concat方法不修改原數(shù)組,只會(huì)返回一個(gè)淺復(fù)制了原數(shù)組中的元素的一個(gè)新數(shù)組。

原數(shù)組的元素會(huì)按照下述規(guī)則拷貝:

  1. 如果該元素是個(gè)對(duì)象引用(不是實(shí)際的對(duì)象),slice 會(huì)拷貝這個(gè)對(duì)象引用到新的數(shù)組里。兩個(gè)對(duì)象引用都引用了同一個(gè)對(duì)象。如果被引用的對(duì)象發(fā)生改變,則新的和原來(lái)的數(shù)組中的這個(gè)元素也會(huì)發(fā)生改變。
  2. 對(duì)于字符串、數(shù)字及布爾值來(lái)說(shuō)(不是 String、Number 或者 Boolean 對(duì)象),slice 會(huì)拷貝這些值到新的數(shù)組里。在別的數(shù)組里修改這些字符串或數(shù)字或是布爾值,將不會(huì)影響另一個(gè)數(shù)組。

五、深拷貝的實(shí)現(xiàn)方式

1、JSON.parse(JSON.stringify())

let arr = [1, 2, { name: "king" }];
let arr2 = JSON.parse(JSON.stringify(arr));
arr2[1] = 10;
arr2[2].name = "偶爾平凡";
console.log(arr);
console.log(arr2);
//[ 1, 2, { name: 'king' } ]
//[ 1, 10, { name: '偶爾平凡' } ]

這種方法雖然可以實(shí)現(xiàn)數(shù)組或?qū)ο笊羁截?,但不能處理函?shù)。這是因?yàn)?JSON.stringify() 方法是將一個(gè)JavaScript值(對(duì)象或者數(shù)組)轉(zhuǎn)換為一個(gè)JSON字符串,不能接受函數(shù)。

let arr = [1, 2, { name: "king", fn: function() {} }];
let arr2 = JSON.parse(JSON.stringify(arr));
console.log(arr);
console.log(arr2);
//[ 1, 2, { name: 'king', fn: [Function: fn] } ]
//[ 1, 2, { name: 'king' } ]

2、手寫(xiě)遞歸函數(shù)實(shí)現(xiàn)

第1步:假設(shè)遞歸函數(shù)已寫(xiě)好。

const deepClone = (value) => {
}

第2步:處理第一層的情況,也就是一般的情況。比如對(duì)方傳遞了一個(gè)null或者undefined進(jìn)來(lái)可以處理的。

const deepClone = (value) => {
  if (value == null) {
    return value;
  }
};
let result = deepClone(null);
let result1 = deepClone(undefined);
console.log(result,result1);//null undefined

第3步:還是處理一般的情況,比如對(duì)方傳遞了一個(gè)普通數(shù)據(jù)類(lèi)型或許函數(shù),直接返回該值即可搞定。

const deepClone = (value) => {
  if (value == null) {
    return value;
  }
  if (typeof value !== "object") {
    return value;
  }
};
let result = deepClone('abc');
console.log(result);//abc

第4步:還是處理一般的情況,比如對(duì)方傳遞了一個(gè)正則類(lèi)型或許時(shí)間類(lèi)型的值,那么返回一個(gè)新正則或時(shí)間即可。

const deepClone = (value) => {
  if (value == null) {
    return value;
  }
  if (typeof value !== "object") {
    return value;
  }
  if (value instanceof RegExp) {
    return new RegExp(value);
  }
  if (value instanceof Date) {
    return new Date(value);
  }
};

第5步:處理傳進(jìn)來(lái)的是對(duì)象或者數(shù)組的情況了。

const deepClone = (value) => {
  if (value == null) {
    return value;
  }
  if (typeof value !== "object") {
    return value;
  }
  if (value instanceof RegExp) {
    return new RegExp(value);
  }
  if (value instanceof Date) {
    return new Date(value);
  }
  let instance = new value.constructor(); //創(chuàng)建一個(gè)新的空對(duì)象或數(shù)組
  for (let key in value) {
    instance[key] = value[key];
  }
  return instance;
};
let obj = { name: "king", age: 10 };
let result = deepClone(obj);
result.name = "偶爾平凡";
console.log(result);//{ name: '偶爾平凡', age: 10 }
console.log(obj);//{ name: 'king', age: 10 }

第6步:我們上面處理的對(duì)象是單層對(duì)象,如果對(duì)象內(nèi)的值 value[key] 又是一個(gè)對(duì)象怎么辦呢,這時(shí)候,才開(kāi)始考慮遞歸即可。因?yàn)榈谝粚拥那闆r我們已經(jīng)處理好了,第二層重復(fù)第一層就OK了。

//深拷貝
const deepClone = (value) => {
  if (value == null) {
    //排除null 和 undefine的情況,直接返回
    return value;
  }
  if (typeof value !== "object") {
    //基本數(shù)據(jù)類(lèi)型和函數(shù)的情況直接返回即可
    return value;
  }
  if (value instanceof RegExp) {
    //正則的情況,返回新的正則即可
    return new RegExp(value);
  }
  if (value instanceof Date) {
    return new Date(value);
  }
  //處理對(duì)象或者數(shù)組的情況,new 創(chuàng)建新的空對(duì)象或數(shù)組
  let instance = new value.constructor();
  for (let key in value) {
    if (value.hasOwnProperty(key)) {
      //排除原型鏈上的屬性或方法
      instance[key] = deepClone(value[key]);
    }
  }
  return instance;
};
let obj = { name: "king", age: 10, se: { sex: 0 }, fn: () => {} };
let newobj = deepClone(obj);
newobj.name = '偶爾平凡';
newobj.se.sex = 1;
console.log(newobj);
console.log(obj);
//{ name: '偶爾平凡', age: 10, se: { sex: 1 }, fn: [Function: fn] }
//{ name: 'king', age: 10, se: { sex: 0 }, fn: [Function: fn] }

確實(shí),實(shí)現(xiàn)了深拷貝,感覺(jué)很完美,但是,遇到下面這種情況,會(huì)直接跑死,進(jìn)入死循環(huán)了。

let obj = { a: 1 };
obj.b = obj;
let newobj =deepClone(obj);
//RangeError: Maximum call stack size exceeded 內(nèi)存爆裂而亡

最終修改后的內(nèi)容

const deepClone = (value, hash = new WeakMap => {
  if (value == null) {
    //排除null 和 undefine的情況,直接返回
    return value;
  }
  if (typeof value !== "object") {
    //基本數(shù)據(jù)類(lèi)型和函數(shù)的情況直接返回即可
    return value;
  }
  if (value instanceof RegExp) {
    //正則的情況,返回新的正則即可
    return new RegExp(value);
  }
  if (value instanceof Date) {
    return new Date(value);
  }
  //處理對(duì)象或者數(shù)組的情況,new 創(chuàng)建新的空對(duì)象或數(shù)組
  let instance = new value.constructor();
  if (hash.has(value)) {
    //在hash 中查詢(xún)一下是否存在過(guò),如果存在就把以前拷貝的返回
    return hash.get(value);
  }
  hash.set(value, instance); //沒(méi)有存過(guò)就放進(jìn)去
  for (let key in value) {
    if (value.hasOwnProperty(key)) {
      //排除原型鏈上的屬性或方法
      instance[key] = deepClone(value[key], hash);
      //將hash繼續(xù)傳遞下去,保證每次能拿到以前拷貝的結(jié)果
    }
  }
  return instance;
};

let obj = { a: 1 };
obj.b = obj;
let newobj = deepClone(obj);
console.log(newobj);
//{ a: 1, b: [Circular] }

注意小知識(shí)點(diǎn):

//字典 Map 和 WeakMap 區(qū)別
//Map 中的key為對(duì)象,如果該對(duì)象外部人工銷(xiāo)毀了,該對(duì)象在Map中并沒(méi)有銷(xiāo)毀
//堆內(nèi)存存放的 { name: "king" } 的地址為 OXFF00
// 地址OXFF00 分別賦值給了 obj以及map中去
//變量obj重新賦值了null;但是map中的地址還在,所以堆內(nèi)存中的對(duì)象不會(huì)銷(xiāo)毀
let obj = { name: "king" };
let map = new Map();
map.set(obj, 0);
obj = null;
console.log(map);
console.log(obj);
//Map { { name: 'king' } => 0 }
//null
//obj 重新賦值后,WeakMap中的obj也改變,也就是若引用
let obj = { name: "king" };
let map = new WeakMap();
map.set(obj, 0);
obj = null;
console.log(map);
console.log(obj);
//WeakMap { <items unknown> }
//null

六、遞歸基礎(chǔ)知識(shí)

1、什么是遞歸

在JavaScript程序中,函數(shù)直接或間接調(diào)用自己。通過(guò)某個(gè)條件判斷跳出結(jié)構(gòu),有了跳出才有結(jié)果。

2、遞歸寫(xiě)法的步驟

  1. 假設(shè)遞歸函數(shù)已經(jīng)寫(xiě)好了。
  2. 尋找遞推關(guān)系。
  3. 將遞推關(guān)系的結(jié)構(gòu)轉(zhuǎn)換為遞歸體
  4. 將臨界條件加入到遞歸體中(一定要加臨界條件,某則陷入死循環(huán),內(nèi)存泄漏)

3、遞歸示例

求1-100的和,用遞歸該怎么寫(xiě)呢?按照遞歸的步驟來(lái)即可
1、假設(shè)遞歸函數(shù)已經(jīng)寫(xiě)好了,即為sum(100),就是求1-100的和。
2、尋找遞推關(guān)系,就是nn-1的關(guān)系 sum(n) ==sum(n-1) +n

let result = sum(100);
let result = sum(99) +100;

3、將遞歸結(jié)構(gòu)轉(zhuǎn)換為遞歸體。

function sum(n) {
  return sum(n - 1) + n;
}

4、加入臨界條件,防止死循環(huán)。求100 轉(zhuǎn)換為 求99 求99 轉(zhuǎn)換為 求98 求98 轉(zhuǎn)換為 求97 … 求2 轉(zhuǎn)換為 求1 求1 轉(zhuǎn)換為 求1 即 sum(1) = 1

function sum(n) {
  if (n == 1) return 1;
  return sum(n - 1) + n;
}
console.log(sum(100));//5050
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容