用Object.assign如何实现混入?

121 阅读5分钟

前言

谈到混入模式,最先想到是不是Vue中Vue.mixin全局API喃?但又可否知道Object.assign设计的初衷也是为了完成混入模式喃?

相比于Object上的其他方法,Object.assign在代码中出现的次数可谓是层出叠见,但是你真懂它的使用和原理吗?

不懂也不要紧,看完此篇也就了然于胸啦!!!

基本用法

  assign(target: object, ...source: object[]): object;

中文介绍:assign(目标对象,...源对象[]):目标对象

功能介绍:将源对象(一个或多个)所有可枚举的属性复制到目标对象中。它将返回目标对象

target参数不可传递nullundefind,会抛出Cannot convert undefined or null to object错误

let target = {
    name: "李白"
}
let source = {
    age: 18
}
let newTarget = Object.assign(target, source)
console.log(newTarget)              // {name: "李白", age: 18}

image-20230216212919705

合并规则

新返回的目标对象和传入的目标对象是相同的对象

let target = {
    name: "李白"
}
let source = {
    age: 18
}
let newTarget = Object.assign(target, source)
console.log(newTarget === target)       // true

目标对象不是对象时,会被包装为对象继续进行合并

Object.assign("abc", {
    age: 18
})                      // String {"abc", age: 18}

合并属性时,如果源对象属性存在 [[Get]] 或目标对象属性的 [[Set]],它会调用对应的gettersetter方法

let target = {
    set uid(value) {
        console.log("target的set方法被调用~~~");
    }
}
let source = {
    get uid() {
        console.log("source的get方法被调用~~~");
    }
}

Object.assign(target, source)
// source的get方法被调用~~~   
// target的set方法被调用~~~

注意:合并时会把源对象属性执行getter方法得到的值赋给目标对象,并不是把源对象属性执行getter属性描述符赋给目标对象

let target = {}
let source = function () {
    let _uid = 0
    return {
        get uid() {
            return ++_uid
        }
    }
}()

let newTarget = Object.assign(target, source)
console.log(newTarget);     // {uid: 1}
console.log(source.uid);   // 2
console.log(newTarget.uid); // 1
console.log(source.uid);   // 3
console.log(newTarget.uid); // 1

可以发现newTarget.uid会始终保持不变了,因为newTarget其实是没有getter方法的

如果目标对象属性不可写,会抛出错误,但是不会影响之前添加的属性

let target = {}
Object.defineProperty(target, 'name', {
    value: "李白",
    enumerable: true,
    configurable: true,
    writable: false
})

let source = {
    age: 18,
    sex: "男",
    name: "杜甫"
}

try {
    Object.assign(target, source)
} catch (error) {

}
console.log(target);    // {name: "李白", age: 18, sex: "男"}

多个源对象遵循从左向右的顺序依次与目标对象合并

let target = {
    a: 1
}
let source1 = {
    b: 2
}
let source2 = {
    c: 3
}
Object.assign(target, source1, source2)     // {a: 1, b: 2, c: 3}

目标对象和源对象具有相同的属性名,则目标对象的属性值将被源对象的属性值覆盖

let target = {
    a: 1
}
let source1 = {
    a: 2,
    b: 3
}
let source2 = {
    b: 5,
    c: 6
}
Object.assign(target, source1, source2)     // {a: 2, b: 5, c: 6}

目标对象只会合并源对象自身的属性,并不会合并源对象原型上的属性

let target = {
    name: "李白"
}

let sourceProto = {
    sex: "男"
}
let source = {
    __proto__: sourceProto,
    age: 18
}

let newTarget = Object.assign(target, source)
console.log(newTarget.age)              // 18
console.log(newTarget.sex)              // undefined

目标对象只会合并源对象自身的可枚举属性和[Symbol的属性

let target = {
    name: "李白"
}

let source = {
    age: 18,
    [Symbol('info')]: "信息"
}
Object.defineProperty(source, 'sex', {
    value: "男",
    enumerable: false
})

let newTarget = Object.assign(target, source)
console.log(newTarget);                 // {name: "李白", age: 18, Symbol(info): "信息"}
console.log(newTarget.age)              // 18
console.log(newTarget.sex)              // undefined

实现原理

上面的基本使用已经把Object.assign解析的很完整了,其实自己可以先动手写一下原理代码

(<any>Object).myAssign = function (target: object, ...sources: object[]): object {
   // 遍历多个源对象
   sources.forEach((currentSource) => {
       // 取源对象所以的可遍历属性
       let currentSourceKeys = Object.keys(currentSource)
       // 源对象合并到目标对象
       currentSourceKeys.reduce((target, curSourceKey) => {
           target[curSourceKey] = currentSource[curSourceKey]
           return target
       }, target)
   })
   // 返回源对象
   return target
};

是不是感觉实现的很完美喃?来测试测试上面的8条和并规则吧

let target = {
    name: "李白"
}
let sourceProto = {
    sex: "男"
}
let source1 = {
    __proto__: sourceProto,
    age: 18,
    name: "白居易"
}
let source2 = {
    name: "杜甫",
    info: "介绍~~~",
    get UID() {
        console.log("source2的UID属性get方法被访问");
        return "1999-01-01"
    }
}

let newTarget = Object.myAssign(target, source1, source2)

/**
 * 验证规则:newTarget和target相同(符合)
 */
console.log(target === newTarget);      // true
/**
 * 验证规则:不会合并source1原型上的属性(符合)
 */
console.log(newTarget.sex);             // undefined
/**
 * 验证规则:source2和target具有相同的属性名name,source2覆盖了target的属性值(符合)
 */
console.log(source2.name);          // 李白
console.log(newTarget.name);        // 杜甫
/**
 * 验证规则:source1和source2从左向右的顺序依次与目标对象合并(符合)
 */
console.log(source1.name);         // 白居易
console.log(source2.name);         // 李白
console.log(newTarget.name);       // 杜甫
/**
 * 验证规则:在合并时,target只会合并source2的UID属性描述符getter的值,而不是合并getter(符合)
 */
console.log(Object.getOwnPropertyDescriptor(source2, "UID"))
// {set: undefined, get: ƒ, enumerable: true, configurable: true,}
console.log(Object.getOwnPropertyDescriptor(newTarget, "UID"));
// {value: '1999-01-01', writable: true, enumerable: true, configurable: true}

8条规则满足了5条,这妥妥的成功一大半呀~~~

在看看其他不符合的吧!!!

/**
 * 验证规则:目标对象不是对象时,会被包装为对象继续进行合并(不符合)
 * target没有被封装成对象
 */
let target = "abc"
Object.assign(target, {
    age: 18
})                      // abc

可以通过Object()函数把基本类型数据转为对象类型,Object.myAssign接受target参数过后,给target包装一层:target = Object(target)

/**
 *  验证规则:如果目标对象属性不可写,会抛出错误,但是不会影响之前添加的属性(不符合)
 * 	并没有报错
 */
let target = Object.defineProperty({}, 'name', {
    value: "李白",
    enumerable: true,
    configurable: true,
    writable: false
})

let source = {
    name: "杜甫"
}

let myTarget = Object.myAssign(target, source)

console.log(myTarget);

只有在严格模式下,向不可写的属性写入值才会报错,所以给Object.myAssign加上严格模式

/**
 * 验证规则:目标对象会合并源对象的[`Symbol`的属性(不符合)
 */
let target = {}
let source = {
    [Symbol('info')]: "信息"
}

let newTarget = Object.myAssign(target, source)
console.log(newTarget);                 // {}

通过Object.getOwnPropertySymbols获取源对象的[Symbol的属性,进行单独合并

最终版源码

(<any>Object).myAssign = function (target: object, ...sources: object[]): object {
    "use strict";

    // 转为对象
    target = Object(target)

    // 遍历多个源对象
    sources.forEach((currentSource) => {
        // 取源对象所以的可遍历属性
        let currentSourceKeys = Object.keys(currentSource)
        // 取源对象所以的Symbols属性
        let currentSourceSymbols = Object.getOwnPropertySymbols(currentSource);
        let allKey = ([...currentSourceKeys, ...currentSourceSymbols])

        // 源对象合并到目标对象
        allKey.reduce((target, curSourceKey) => {
            target[curSourceKey] = currentSource[curSourceKey]
            return target
        }, target)
    })

    // 返回源对象
    return target
};

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 16 天,点击查看活动详情