0%

学习笔记 2020 10 23

学习笔记 2020-10-23

JavaScript 高级程序设计(第4版) 阅读记录

代理与反射

代理基础

代理是目标对象的抽象。

使用 Proxy 构造函数创建代理。这个构造函数接收两个参数:目标对象和处理程序对象。两个参数都是必须的。

const target = {
  id: 'target'
};
const handler = {};
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 给目标属性赋值会反映在两个对象上
// 因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(proxy.id); // foo
// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(proxy.id); // bar
// hasOwnProperty()方法在两个地方
// 都会应用到目标对象
console.log(target.hasOwnProperty('id')); // true
console.log(proxy.hasOwnProperty('id')); // true
// Proxy.prototype 是 undefined
// 因此不能使用 instanceof 操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false

使用代理的主要目的是可以定义 捕获器 。捕获器就是在处理程序对象中定义的 “基本操作的拦截器” 。每次在代理对象上调用捕获器对应的基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

定义 get() 捕获器。在代理对象上 proxy[property]proxy.propertyobject.create(proxy)[property] 等操作都会触发基本的 get() 操作以获取属性。

捕获器接收三个参数,目标对象、要查询的属性和代理对象。

const target = {
  foo: 'bar'
};
const handler = {
  get(trapTarget, property, receiver) {
    console.log(trapTarget === target);
    console.log(property);
    console.log(receiver === proxy);
  }
};
const proxy = new Proxy(target, handler);
proxy.foo;
// true
// foo
// true

可以使用全局 Reflect 对象的同名方法来重建原始操作。

处理程序对象中所有可以捕获的方法都有对应的 Reflect API 方法。这些方法与拦截器捕获的方法具有相同的名称和函数签名,具有与被拦截方法相同的行为。

const target = {
  foo: 'bar'
};
const handler = {
  get() {
    return Reflect.get(...arguments);
  }
};
// const handler = { 
//   get: Reflect.get 
// };
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar

创建一个可以捕获所有方法,每个方法都转发给对应反射 API 的空代理,只需要:

const target = {
  foo: 'bar'
};
const proxy = new Proxy(target, Reflect);
console.log(proxy.foo); // bar
console.log(target.foo); // bar

捕获器不变式

捕获处理程序的行为必须遵守 ”捕获器不变式“

例如,目标对象有一个不可配置且不可写的数据属性。捕获器在返回一个与该属性不同的值时,会抛出 TypeError

可撤销代理

普通代理对象与目标对象之间的联系会在代理对象的生命周期内一直持续存在。

Proxy 暴露了 revocable() 方法。这个方法支持撤销代理对象与目标对象的关联。撤销代理的操作是不可逆的。撤销函数是幂等的,调用多少次的结果都一样。撤销代理之后再次调用代理会抛出 TypeError

const target = {
  foo: 'bar'
};
const handler = {
  get() {
    return 'intercepted';
  }
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke();
console.log(proxy.foo); // TypeError

实用反射 API

  1. 反射 API 和对象 API

    反射 API 并不限于捕获处理程序。大多数反射 APIObject 类型上有对应的方法。

  2. 状态标记

    很多反射方法返回称作 ”状态标记“ 的布尔值,表示意图执行的操作是否成功。

    // 初始代码
    const o = {};
    try {
      Object.defineProperty(o, 'foo', 'bar');
      console.log('success');
    } catch (e) {
      console.log('failure');
    }
    // 重构后的代码
    const o = {};
    if (Reflect.defineProperty(o, 'foo', { value: 'bar' })) {
      console.log('success');
    } else {
      console.log('failure');
    }

    以下方法都会提供状态标记:

    • Reflect.defineProperty()
    • Reflect.preventExtensions()
    • Reflect.setPrototypeOf()
    • Reflect.set()
    • Reflect.deleteProperty()
  3. 用一等函数替代操作符

    以下反射方法提供只有通过操作符才能完成的操作:

    • Reflect.get() 可以替代对象属性访问操作符。
    • Reflect.set() 可以替代 = 赋值操作符。
    • Reflect.has() 可以替代 in 操作符或 with()
    • Reflect.deleteProperty() 可以替代 delete 操作符。
    • Reflect.construct() 可以替代 new 操作符。
  4. 安全地应用函数

    在通过 apply 方法调用函数时,被调用的函数可能也定义了自己的 apply 属性。为绕过这个问题,可以使用定义在 Function 原型上的 apply 方法,比如:

    Function.prototype.apply.call(myFunc, thisVal, argumentList);

    改为:

    Reflect.apply(myFunc, thisVal, argumentsList);

代理另一个代理

通过代理另一个代理,可以形成多层代理:

const target = {
  foo: 'bar'
};
const firstProxy = new Proxy(target, {
  get() {
    console.log('first proxy');
    return Reflect.get(...arguments);
  }
});
const secondProxy = new Proxy(firstProxy, {
  get() {
    console.log('second proxy');
    return Reflect.get(...arguments);
  }
});
console.log(secondProxy.foo);
// second proxy
// first proxy
// bar

代理的问题与不足

  1. 代理的 this 问题

    方法中的 this 指向调用这个方法的对象。

    const target = {
      thisValEqualsProxy() {
        return this === proxy;
      }
    };
    const proxy = new Proxy(target, {});
    console.log(target.thisValEqualsProxy()); // false
    console.log(proxy.thisValEqualsProxy()); // true

    调用代理上的任何方法,比如 proxy.outerMethod() ,而这个方法又会调用另一个方法, this.innerMethod() 。实际上会调用 proxy.innerMethod() 。当目标对象依赖于对象标识,就会出现意外。

    const wm = new WeakMap();
    class User {
      constructor(userId) {
        wm.set(this, userId);
      }
      set id(userId) {
        wm.set(this, userId);
      }
      get id() {
        return wm.get(this);
      }
    }

    当一个 WeakMap 实例被代理。

    const wm = new WeakMap();
    class User {
      constructor(userId) {
        wm.set(this, userId);
      }
      set id(userId) {
        wm.set(this, userId);
      }
      get id() {
        return wm.get(this);
      }
    }
    const user = new User(123);
    console.log(user.id); // 123
    const userInstanceProxy = new Proxy(user, {});
    console.log(userInstanceProxy.id); // undefined

    因为 User 实例一开始使用目标对象作为 WeakMap 的键。代理对象尝试从自身取得这个实例。换句话说,构造函数中设置 wm 中的键名为 user 实例。代理对象获取时却使用自身来作为键名获取,自然无法获取。

    const wm = new WeakMap();
    class User {
      constructor(userId) {
        wm.set(this, userId);
      }
      set id(userId) {
        wm.set(this, userId);
      }
      get id() {
        return wm.get(this);
      }
    }
    const UserClassProxy = new Proxy(User, {});
    const proxyUser = new UserClassProxy(456);
    console.log(proxyUser.id); // 456

    把代理 User 实例改为代理 User 类本身。此时使用的键名就是代理的实例。

  2. 代理与内部槽位

    代理与内置引用类型的实例通常可以很好地协同。但有些内置类型可能会依赖代理无法控制的机制,导致在代理上调用某些方法会出错。

    例如 Date 类型。根据 ECMAScript 规范,该类型方法的执行依赖 this 值上的内部槽位 [[NumberDate]] 。代理对象上不存在这个内部槽位,这个内部槽位的值也不能通过普通的 get()set() 操作访问到。

    const target = new Date();
    const proxy = new Proxy(target, {});
    console.log(proxy instanceof Date); // true
    proxy.getDate(); // TypeError: 'this' is not a Date objec

MDN 学习记录

图像、媒体和表单元素

使用 object-fit 属性,可以设置替换元素以不同方式调整到合乎盒子的大小。

  • cover 使图片覆盖盒子
  • contain 使图片完全展示在盒子内部
  • fill 不按比例放缩来覆盖盒子

在一些浏览器中,表单元素默认不会继承字体样式。

不同浏览器中,对于 form 元素使用不同的盒子模型。

对表格设置 css 属性 table-layoutfixed ,然后可以自定义 thead 中的单元格宽度。