import isobj from './isobj.mjs'
//eq, SameValueZero(同lodash eq)
function eq(a, b) {
return a === b || (Number.isNaN(a) && Number.isNaN(b))
}
//isObjectLike, 非null之物件(含陣列、Date、Map、類別實例)
function isObjectLike(v) {
return typeof v === 'object' && v !== null
}
//isPlain, 純物件: 原型為Object.prototype或null(含跨realm之原生Object原型), 判定不看Symbol.toStringTag(同lodash isPlainObject之意圖, 但不受不可寫之偽標籤影響); 陣列、Date、Map、類別實例等為非純物件
function isPlain(v) {
if (!isObjectLike(v)) {
return false
}
let p = Object.getPrototypeOf(v)
if (p === null || p === Object.prototype) {
return true
}
if (Object.getPrototypeOf(p) !== null) {
return false
}
let c = Object.prototype.hasOwnProperty.call(p, 'constructor') ? p.constructor : undefined
return typeof c === 'function' && Function.prototype.toString.call(c) === Function.prototype.toString.call(Object)
}
//copyDest, 目的端之複本: 只複製陣列(保留空洞)與純物件(保留原型為null或Object.prototype, 複製自有可列舉之字串鍵與Symbol鍵), 其餘值(類別實例、函數、Map、Set、Date、TypedArray、ArrayBuffer…)取參照, 保留其同一性(含私有欄位);
// seen記錄已複製之容器以處理循環參照與同一容器被多處引用(複本內仍共用同一個複本)
function copyDest(v, seen) {
if (Array.isArray(v)) {
if (seen.has(v)) {
return seen.get(v)
}
let out = new Array(v.length)
seen.set(v, out)
for (let i = 0; i < v.length; i++) {
if (i in v) {
out[i] = copyDest(v[i], seen)
}
}
return out
}
if (isPlain(v)) {
if (seen.has(v)) {
return seen.get(v)
}
let out = Object.getPrototypeOf(v) === null ? Object.create(null) : {}
seen.set(v, out)
for (let k of Object.keys(v)) {
setKey(out, k, copyDest(v[k], seen))
}
for (let sym of Object.getOwnPropertySymbols(v)) {
if (Object.prototype.propertyIsEnumerable.call(v, sym)) {
out[sym] = copyDest(v[sym], seen)
}
}
return out
}
return v
}
//copyInstance, 類別實例(isobj為真之非純物件)作為合併基底時的寫入時複製: 新物件沿用其原型, 複製自有可列舉鍵(值經copyDest), 不修改呼叫端之實例(私有欄位與內部槽無法複製, 僅出現在被合併之實例的複本上)
function copyInstance(d, seen) {
let out = Object.create(Object.getPrototypeOf(d))
for (let k of Object.keys(d)) {
setKey(out, k, copyDest(d[k], seen))
}
for (let sym of Object.getOwnPropertySymbols(d)) {
if (Object.prototype.propertyIsEnumerable.call(d, sym)) {
out[sym] = copyDest(d[sym], seen)
}
}
return out
}
//讀取容器自有鍵之值: 只讀自有屬性(繼承而來之容器不併入, 以自有屬性遮蔽, 免得就地改到由原型共用之呼叫端容器); 鍵constructor之值為函數時視為不存在(同lodash merge之safeGet)
function getKey(o, k) {
if (!Object.prototype.hasOwnProperty.call(o, k)) {
return undefined
}
if (k === 'constructor' && typeof o[k] === 'function') {
return undefined
}
return Object.getOwnPropertyDescriptor(o, k).value
}
//設定容器之鍵值, 鍵__proto__以defineProperty設為自有屬性(一般賦值會改原型)
function setKey(o, k, v) {
if (k === '__proto__') {
Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true })
}
else {
o[k] = v
}
}
//合併值d與s並回傳結果, d為可就地修改之複本(copyDest的產物或類別實例的寫入時複製), s為來源(不修改, 呼叫端保證s不為undefined: 最外層由ltdtmerge過濾、內層於迴圈內處理), stack記錄正在處理之來源容器與其對應結果(處理循環參照)
// s為陣列: 基底為d(d為陣列時)否則新陣列, 依索引逐一合併; s為純物件或最外層之物件列(top): 基底為d(d為非陣列之物件時)否則新物件, 依自有與繼承之可列舉字串鍵逐一合併,
// length與其他鍵同為一般鍵(不得使用lodash merge/each等對具length鍵之物件採陣列樣迭代之函數, 見R-OBJ-ITER-01); TypedArray/Buffer取複本; 其他(原始值、Date、Map、類別實例、函數等)直接取s
function mergeVal(d, s, ctx, top = false) {
let stack = ctx.stack
//來源為陣列
if (Array.isArray(s)) {
if (stack.has(s)) {
return stack.get(s)
}
let base = Array.isArray(d) ? d : [] //種類不同(d為物件或原始值)以來源為準
stack.set(s, base)
for (let i = 0; i < s.length; i++) {
let sv = s[i]
if (sv === undefined) {
if (!(i in base)) {
base[i] = undefined
}
}
else {
let cur = getKey(base, i)
let nv = mergeVal(cur, sv, ctx)
if (!eq(cur, nv)) { //SameValueZero相等(含-0與0)時不重新賦值, 同lodash merge
base[i] = nv
}
}
}
stack.delete(s)
return base
}
//來源為純物件, 最外層(top)是被合併的資料列, lodash merge不論其型別與原型皆依鍵合併(Date、Map、類別實例之自有可列舉鍵亦然), 故最外層放寬為任何非陣列之物件
if (isPlain(s) || (top && isObjectLike(s))) {
if (stack.has(s)) {
return stack.get(s)
}
let base = isPlain(d) ? d : (isobj(d) ? copyInstance(d, ctx.seen) : {}) //d為純物件(已是複本)則沿用; 類別實例寫入時複製; 其餘種類(陣列、Date、Map、原始值等)以來源為準
stack.set(s, base)
for (let k in s) {
let sv = s[k]
if (sv === undefined) {
if (!(k in base)) {
setKey(base, k, undefined)
}
}
else {
let cur = getKey(base, k)
let nv = mergeVal(cur, sv, ctx)
if (!eq(cur, nv)) { //同上
setKey(base, k, nv)
}
}
}
stack.delete(s)
return base
}
//TypedArray/Buffer取複本(同lodash merge複製typed array, 結果不與輸入共用參照)
if (ArrayBuffer.isView(s) && !(s instanceof DataView) && typeof s.constructor.from === 'function') {
return s.constructor.from(s)
}
return s
}
/**
* 針對物件陣列ltdt1與ltdt2逐一深度合併
*
* 回傳新物件陣列,不修改ltdt1與ltdt2(含其內巢狀陣列與物件),結果內之陣列與純物件與輸入無共用參照(TypedArray/Buffer取複本,Date、Map、類別實例等其他非純物件之值直接取用)。
* 合併規則:同路徑之陣列依索引合併、純物件依鍵合併;ltdt2之值為undefined時保留ltdt1之值(ltdt1無該鍵時設為undefined);同路徑之種類不同(陣列與物件、物件與原始值等)時以ltdt2為準;ltdt2之資料列非陣列非物件(undefined、null、原始值)時忽略。
* 物件之length與其他鍵同為一般鍵,不會使該物件被當作陣列處理;鍵__proto__與constructor不會污染原型;ltdt1之原型所共用之容器不會被修改(只讀自有鍵之值)。
* ltdt1與ltdt2非陣列或長度不同時回傳空陣列。
*
* Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/ltdtmerge.test.mjs Github}
* @memberOf wsemi
* @param {Array} ltdt1 輸入資料物件陣列
* @param {Array} ltdt2 輸入資料物件陣列,若與ltdt1內物件有重複的欄位值,優先保留ltdt2內物件的欄位值
* @returns {Array} 回傳處理後物件陣列
* @example
*
* console.log(ltdtmerge([{ a: 12, b: 34.56 }, {}], [{ a: '123', c: 'mn' }, { aa: 'a123', bb: 'bmn' }]))
* // => [{ a: '123', b: 34.56, c: 'mn' }, { aa: 'a123', bb: 'bmn' }]
*
* console.log(ltdtmerge([{ x: [1, 2, 3], y: { length: 2, a: 1 } }], [{ x: [9], y: { length: 5, b: 2 } }]))
* // => [{ x: [9, 2, 3], y: { length: 5, a: 1, b: 2 } }]
*
*/
function ltdtmerge(ltdt1, ltdt2) {
//check
if (!Array.isArray(ltdt1) || !Array.isArray(ltdt2)) {
return []
}
if (ltdt1.length !== ltdt2.length) {
return []
}
//每個元素以ltdt1之深層複本為目的端合併ltdt2, 不修改輸入
let r = []
for (let i = 0; i < ltdt1.length; i++) {
let seen = new Map() //每列各自一份, 列與列之間不共用複本
let d = copyDest(ltdt1[i], seen)
let s = ltdt2[i]
if (Array.isArray(s) || isObjectLike(s)) {
r.push(mergeVal(d, s, { stack: new Map(), seen }, true))
}
else {
r.push(d) //來源列非陣列非物件(undefined、null、原始值等)時忽略來源, 同lodash merge
}
}
return r
}
export default ltdtmerge