少侠,彩色对象和完美call函数是什么鬼?

654 阅读16分钟

image

少侠们好~

上次的内容中,我们主要讨论了如何控制一个函数被调用时的 this 指向,

在结尾时,我们实现了一个看起来还不错的 call 函数来帮助解决这个问题,

image

这个 call 函数虽然能够满足大部分情况,

但是我们也提到了,比起系统内置的 call 函数,还是有一些区别。

主要是在遇见一些特殊对象,比如冻结对象时,

我们的 call 函数就会失效:

image

当然,除了冻结对象以外,

实际上也还有另外的特殊对象,

也会导致我们的 call 函数失败。

所以,

这次故事的主要内容就是~

我们一起试着找寻一些办法,

看看能不能让我们的 call 函数也解决掉这个问题~

好了,

让我们开始进入正题~


不同颜色的对象(Object)?

在聊 call 函数之前,我们先来聊一聊对象。

这里的对象是 JS 中的对象,不是现实中的对象!

JS 中的对象少侠你应该不陌生,

不够,少侠你玩过游戏吗?

如果少侠你玩过一些角色扮演游戏的话,

你可能会知道,有些游戏中的装备是有不同颜色的,

比如,白色,蓝色,紫色,红色等等。

而在 JS 中,

对象,其实也可以是有颜色的,

我们其实也可以将对象分为几种不同颜色对象。

同样是,白色对象,蓝色对象,紫色对象,以及红色对象。

白色对象

首先是白色的对象,

这种对象,应该是少侠你最常使用和遇见的,通常你用字面量直接创建的对象,

一开始它就是白色的:

image

白色的对象是最灵活的对象,你可以在它上面随意添加属性,更改属性,以及删除属性。

image

这种对象少侠你应该很熟悉,因为你平时遇见的大多数对象应该都是这种。

在它的基础上,则是蓝色对象。

蓝色对象

蓝色对象比白色对象要稍微严肃一些,它对外来事物比较抵抗,所以,你不能给它添加新的属性

你可以使用 Object.preventExtensions() 函数来将一个白色对象进阶为蓝色对象。

image

少侠你可以看到,一旦你将对象进阶为蓝色对象之后,你就不能给它添加任何新的属性。

但是,如果它之前有属性的话,你也许可以改变属性的值,也可以删除掉已经存在属性。

image

在蓝色之上,则是紫色的对象了。

紫色对象

紫色对象比蓝色对象更加高冷,你不仅不能给它添加新的属性,你也不能删除掉它的属性。

同样的,对于之前就存在的属性,你也许可以改变它的值。

你可以使用 Object.seal() 函数来将一个白色对象进阶为紫色对象。

image

少侠你可以看到,不管我们是给一个紫色对象添加属性,还是试着删除它已经存在的属性,

都失败了。

那么,我们再来试着改变一下 type 的值:

image

好像是可以的,和蓝色对象一样,这里我们也成功改变了 type 的属性。

OK,这里我们就暂时先这样!

继续来看看最后一种颜色的对象。

红色对象

红色对象是最冷酷的对象! 它拒绝一切外来事物,你不能给它添加新的属性,不能删除已经存在的属性,而且,也不能改变现有的任何属性。

你可以使用 Object.freeze() 函数来将一个白色对象进阶为红色对象。

image

够冷酷吧?

一种完全不想理你的感觉。

是不是和少侠你喜欢的妹子一样?


不完善的探测器

OK,现在少侠你已经认识了4个不同颜色对象。

你应该也知道了,除了你经常使用的白色对象之外,其实还有一些特殊的对象。

只不过它们平时很少外出,所以你很少遇见它们。

在 JS 中,它们实际上分别叫做

不可扩展对象,不能添加新属性的对象,也就是上面的蓝色对象

密封对象,既不能添加新属性,也不可删除属性的对象,也就是紫色对象

冻结对象,既不能添加新属性,也不能删除属性,也不能改变原有属性的对象,也就是上面的红色对象

但是现在你可能会有一个问题,那就是,既然都遇见了,如何才能识别一个对象的颜色呢?

比如如何知道一个对象到底是蓝色,紫色,或是红色?

是不可扩展对象,密封对象,还是冻结对象?

你可以使用下面几个工具来检测一个对象是否拥有某些特点,

不可扩展对象:

image

密封对象:

image

冻结对象:

image

注意了,

少侠,

我上面注释的内容并不是说这几个函数能直接检测出某个对象是属于不可扩展对象,密封对象,或是冻结对象

而是说它能检测某个对象是否拥有某个颜色对象的特点

为什么这样说呢?

如果少侠你重新回顾上面的不同颜色对象的话,你可能会发现,

假如一个对象是冻结对象的话,那么它其实也拥有密封对象的特点,比如冻结对象不能添加新属性,不能删除属性,密封后的对象同样也不行。

同时它也拥有不可扩展对象的特点,比如不可扩展对象也不能添加新属性。

也就是,

不可扩展对象 -> 密封对象 -> 冻结对象,实际上是一个一个进阶的关系,后面的包含了前面的特点,但是更进一步。

就和一些角色扮演类游戏里面少侠你升级强化装备一样,后面的装备属性包含了前面的属性。

所以,

当你的对象是一个冻结对象时,

下面的结果都是 true:

image

而当一个对象是密封对象时,

下面的结果都是 true:

image

但是它不满足冻结对象的条件,因为冻结对象的属性不可以修改,它不满足这一点:

image

所以上面几个道具实际上并不算很完善的探测器。

要真正检测出某个具体对象类型,我们需要结合使用这几个方法。

检测一个对象是否是不可扩展对象

image

检测是否是密封对象

image

检测是否是冻结对象

image

完全OK~

现在少侠你知道了对象其实是有不同颜色(类型)的,也知道了如何准确检测出它属于哪个颜色。

现在让我们回头来看之前的 call 函数,

少侠你应该知道我们的问题所在了,

我们之前重点关注了 fn, 但是忽略了我们的 obj。

也就是说,

image

这一行代码,

如果碰巧我们的对象是不可扩展对象,密封对象,或冻结对象的话,

是可能会失败的。

最后的曙光

我们来看看哪些情况可能会失败。

对于蓝色对象(不可扩展对象)来说,

它不可以添加新属性,但是可以改变已经存在的属性。

所以对于一个之前不存在 fn 属性的蓝色对象来说,

我们的 call 函数会失败:

image

由于 user 本身没有 fn 属性,

当我们试着向 obj.fn 赋值时候,就是在添加一个新属性,

但是蓝色不可扩展对象不允许添加新属性,所以就会失败。

但是对于有 fn 属性的蓝色不可扩展对象来说,却可以:

image

这里,由于 user 本身之前有一个 fn 属性,

当我们试着进行 obj.fn = fn 时,我们是在修改属性。

蓝色不可扩展对象一般是允许修改属性的,所以这里我们成功了。

紫色对象(密封对象)和蓝色对象也是一样,都会遇见这样的情况。

而对于红色对象(冻结对象)来说,

无论什么情况,

我们都没有办法直接在它上面执行 obj.fn = fn 这个操作。

因为它拒绝任何添加,修改,删除等操作。

到这里,少侠你是不是有种感觉。

都是些什么乱七八糟的对象?

有没有简单点的办法一次性解决这几种不同的情况?

这次不留坑了,

直接告诉少侠你答案。

有!

而且之前少侠你才刚认识了它!

再遇 mixin

少侠还记得之前遇见的 mixin 吗?

利用 mixin 我们可以把一个对象,混入到另一个对象,来完成对象组合。

image

而如果我们把一个对象 mixin 进一个空对象的话,实际上可以算是简单复制了一份这个对象。

image

少侠你有灵感了没?

没错!

我们可以利用 mixin,将不可扩展对象,密封对象,或是冻结对象混入进一个普通的白色对象,然后对这个新的对象使用我们的 call 函数。

image

这个方法能够适用于所有的 3 种特殊对象。

不可扩展对象,密封对象,冻结对象。

因为我们实际上是将他们复制成了一个白色对象,也就是普通对象。

所以我们可以更新一下我们的 call 函数。

image

利用更新后的 call 函数测试一下:

image

等一等,我们是不是还漏掉了什么?

是的,

如果我们的函数不是 getName,

而是 setName 呢?

image

黎明前的黑暗

没错,我们的函数不一定只是读取数据,也可能想要改变数据。

对于普通白色对象来说,我们的 call 函数可以正常工作。

image

而对于其他其他对象的话,由于我们 call 函数内部是复制了一个新对象,

所有操作都是在它上面进行的,所以改变属性也是在这个新对象上改变。

image

对于我们传递进去的obj对象本身,并不会有什么改变。

这个问题对于红色冻结对象来说,倒是不用担心。

因为它本身就不能被改变属性。

即使使用系统的 call 函数也不行。

而对于蓝色不可扩展对象和紫色密封对象来说就不好办了,

因为它们要视具体情况而定,

是添加属性操作?

还是修改属性操作?

如果一个对象它之前有 name 属性,那么对于不可扩展对象和密封对象来说,

应该是可以正常进行修改操作的。

使用JS 中内置的 call 函数试试:

image

但是我们的 call 函数不会有这个效果,

因为它的所有操作都执行在了内部新的对象 safeObj 上。

image

少侠你看到了,我们并没有成功改变掉 user 中的 name 属性。

怎么解决呢?

要不。。。

试着在 call 函数的最后,

再将 safeObj 再混入回 obj 完成更新操作?

image

看起来没有什么问题!

但是!

少侠你可能不知道的是,

Object.assign 在面对不可扩展对象和密封对象时,也是有可能会失效的。

给一个密封对象混入一个已经存在的属性:

image

这里我们针对密封对象使用 Object.assign ,

由于混入的相同属性,属于修改属性,所以成功了。

但是如果我们试图通过混入一些 user 上本身没有的新属性:

image

boom~

失败了!

这种情况对于不可扩展对象同样如此。

所以,我们现在遇见的最大的困难,

就是如何解决掉不可扩展对象和密封对象这两种中间情况的对象,

这种卡在中间,不上不下的东西最难搞定,

就和撩妹一样,很快拒绝和同意的都还好,

忽冷忽热的才真是让你欲仙欲死!

“天辰你跑题了!”

还是继续说 JS 中对象的事!

那么既然 Object.assign 不能直接使用,

我们就得曲线救国,采用迂回战术了!

变异的 mixin

少侠你还记得我们之前手动实现的 mixin 函数吗?

就像这样的一个函数:

image

它和 Object.assign 功能大概一样,都是用来浅复制对象,只不过是我们自己做的,

自己做的好处就是,

我们可以随时对它做一点改变,

加一些定制需求,

让它也可以对不可扩展对象,密封对象,或冻结对象使用。

改变的内容也不是很麻烦,

只需要在最后的 forEach 函数使用一下 try catch :

image

这样的话,我们可以不用关心到底是什么对象,什么情况,

反正该成功的对象,自然会成功,

不成功的,我们也做好了保护措施,让它不会影响到外界。

但行好事,莫问前程!

Just use the try catch,don't care about error!

有趣!

来试一试这个函数:

image

ok,

之所以叫 变异后的 mixin

就是因为它可以对所有对象使用。

更棒的是!

这个变异后的 mixin 函数,可以帮助我们 call 函数更进一步。

image

这样的话, 我们的 call 函数也可以处理改变数据这种情况了

image

还有最后一步!

也就是遇见删除属性函数的时候:

image

删除属性的逻辑和我们上面的思路稍微有点类似,

只需要在最后使用 mixin 混入回去的时候,

我们额外将 safeObj 和 obj 属性对比一下,

如果obj 上存在某个属性, 而safeObj 上不存在这个属性,

则说明我们的函数执行了删除操作,

这个时候,

我们也试着对 obj 执行删除属性操作。

用 name 属性举例,

假如删除了 name 属性:

image

我们对比发现 safeObj 少了一个 name 属性,

这样的话,我们也应该试着删除掉 obj 中的 name 属性。

实际上, safeObj 就好像是一个分身对象,

我们对所有这个分身执行的添加,删除,修改等操作,

在最后一刻,我们都应该对比着还原到我们的 obj 上!

为了还原删除操作,我们需要再对 mixin 做一点点改变:

image

我们主要修改了 2 个地方,一是在 forEach 函数中,

我们对比了两个对象的属性 key,如果 safeObj 没有某个属性,而原始的 obj 有,则说明函数中执行了删除操作,我们就试着对 obj 执行删除操作。

同样,这里我们使用 try catch 来保证不会出现错误。

另外一个修改的地方是Object.getOwnPropertyNames(obj) 我们改为了遍历的obj, 而不是之前的 safeObj。

猜猜为什么要这样做?!!!

完全ok~

现在我们也终于解决了删除属性的问题,

好了,

这就是我们最终的 call 函数了!

完美 call 函数?

image

所以,这是一个完美的 call 函数吗?

不!

实际上,我也不确定它是否哪里还有问题,完美这个词有时候是种假象,它可能与每个人的认知有关。

但是少侠如果你认真阅读了我的内容,

你至少应该知道,比起之前的 call 函数,我们确实更进一步了。

没有完美的东西,

但是少侠你可以不断进步!

我认为这才是最重要的。

好了,

最后再测试一遍。

普通对象:

image

不可扩展对象:

image

密封对象:

image

冻结对象:

image

完全ok!

我们的 call 函数终于支持其他 3 种颜色的对象了~

好了,

恭喜少侠阅读到了这里~

不管怎样,

希望你能有所收获,

好了,

江湖路远,有缘再见~


小酒馆

实际上,对于不可扩展对象,密封对象,和冻结对象的特点,我们这里提到的并不是特别完整,

下面有一些链接,推荐少侠你仔细阅读一下。

不可扩展对象

密封对象

冻结对象

另外,我们的 call 函数,也还有漏掉的地方,比如关于多层嵌套对象的问题,

image

向上面这样针对一个多层对象执行嵌套操作的话,因为我们复制对象时用的 Object.assign,它是浅复制,如果要更进一步的话,实际上我们应该深度复制对象。

感兴趣的话少侠你可以试着自己试试。

如果不清楚浅拷贝和深拷贝的话,少侠你可以自行查阅其他文章,

或者夸我几句,

改天心情好了再单独说说,哈哈。

脑筋急转弯

“有白色,蓝色,紫色,红色对象,为什么没有黄色对象呢???”

因为 。

黄色对象违规被抓走了!

一些你可能关心的问题

1、天辰,实现这样一个更复杂的 call 函数有什么意义? 而且明明已经有内置 call 函数的情况下?

少侠!你真的以为,

这次故事的重点是 call 函数本身吗?

少侠你想一下,

为什么我们能意识到开始的 call 函数有问题?

为什么我们能知道有些地方可以完善?

为什么我们能够解决发现的问题?

call 函数本身不是很重要,

重要的是我们对事物的认知。

比如,如果我们不知道 JS 中还有不可扩展对象,密封对象,和冻结对象的话,

那我们很可能就认为我们上一次结尾的 call 函数已经没有问题了。

如果我们不知道可以用 mixin 来复制对象的话,我们也可能解决不掉发现的问题。

从另外一个角度来看,JS 中的 call 函数我们可以逃避掉,

可以不使用它,

但如果换成现实中的 “call 函数” 呢?

所以,少侠,

好好学习,天天向上~

2、天辰大人,你好大的官威呀!还给我讲道理?

哈哈哈哈,

写偏了写偏了!

我的文章的原则是不抛弃,不放弃,不负责。

如果觉得有用,就听一下,如果觉得没有用,就当看小故事了~

符文之地

最终 call 函数代码:

function call(obj, fn) {
  var safeObj;

  function mixin(obj, safeObj) {
    Object
    .getOwnPropertyNames(obj)
    .forEach(key => {
      if (
        !safeObj.hasOwnProperty(key) && 
        obj.hasOwnProperty(key)
      ) {
        try {
          delete obj[key];
        } catch {}
        return;
      } 

      try {
        obj[key] = safeObj[key];
      } catch {}
    });
  }

  if (
    Object.isFrozen(obj) ||
    Object.isSealed(obj) ||
    !Object.isExtensible(obj)
  ) {
    safeObj = Object.assign({}, obj);
  } else {
    safeObj = obj;
  }

  var savedFn = safeObj.fn;

  safeObj.fn = fn;

  var args = [];
  for (var i = 2; i < arguments.length; i++) {
    args.push('arguments[' + i + ']');
  }

  var argsStr = String(args);
  var fnStr = 'safeObj.fn(' + argsStr + ')';
  var result = eval(fnStr);

  if (savedFn) {
    safeObj.fn = savedFn;
  } else {
    delete safeObj.fn;
  }

  if (!Object.isFrozen(obj)) {
    if (
      Object.isSealed(obj) ||
      !Object.isExtensible(obj)
    ) {
      mixin(obj, safeObj);
    }
  }

  return result;
}