js学习Proxy

目录

Proxy的概念和作用

基本语法与API

使用 Proxy 进行数据劫持

用 Proxy 进行权限控制


Proxy的概念和作用

Proxy是ES6中新增的一个功能,它可以在某个对象前架设一个“拦截器”,从而可以对该对象的访问进行拦截和控制。可以理解为是对对象访问的一个代理,通过代理可以改变对象的默认行为。

Proxy的作用主要有以下几个方面:

1. 对象的拦截和控制:可以对对象的属性访问、赋值、函数调用等操作进行拦截和控制,从而实现对对象行为的定制。

2. 数据劫持:可以通过Proxy实现数据双向绑定、深度监听、表单校验等数据劫持操作。

3. 权限控制:可以使用Proxy实现对象属性的访问权限控制,限制一些敏感属性的访问。

4. 性能优化:可以使用Proxy进行缓存、懒加载和单例模式等性能优化操作。

Proxy和Object.defineProperty都可以用于拦截和控制对象的属性访问,但是它们之间有以下几个区别:

1. Proxy支持拦截更多的操作:Proxy可以拦截更多的对象操作,包括对象属性的读取和设置、函数调用、in操作符、for...in循环等等,而Object.defineProperty只能拦截属性的访问和设置。

2. Proxy是基于对象的拦截:Proxy是基于对象的拦截,即一个Proxy实例对应一个被拦截的对象,通过代理可以改变整个对象的行为。而Object.defineProperty是基于属性的拦截,可以对单个属性进行拦截。

3. Proxy具有别名效应:在不需要拦截的情况下,可以直接使用对象的别名和引用来访问对象。而Object.defineProperty修改对象行为之后,不可以直接使用对象的别名和引用来访问属性。

4. Proxy可以使用Reflect对象:Proxy通过Reflect对象来执行默认操作,而Object.defineProperty则不能。

Proxy作为ES6中新增的一个功能,具有以下优势和劣势:

优势:

1. 更加灵活和强大:Proxy比Object.defineProperty更加灵活和强大,可以拦截并控制更多的对象操作,包括读取和设置属性、函数调用等等。

2. 可以动态修改对象行为:通过修改Proxy的拦截函数,可以动态地改变对象的行为,而Object.defineProperty不能实现这样的功能。

3. 容易扩展:当需要添加新的拦截函数时,可以通过添加新的Proxy拦截器来实现,而不需要对代码进行大规模的修改,开发起来更加容易。

劣势:

1. 兼容性不足:虽然现在很多主流浏览器都支持Proxy,但是一些旧版的浏览器还不支持,因此在实际使用中还需要考虑兼容性的问题。

2. 性能问题:相比Object.defineProperty等原生API,使用Proxy会带来一些额外的性能开销,尤其是在递归拦截、大量拦截操作等复杂场景下,会对程序的性能造成一定影响。但是在大多数场景下,Proxy的性能问题可以被忽略不计。

基本语法与API

Proxy的基本语法和API包括以下几个方面:

1. 基本语法:

let target = {}; // 被拦截的对象
let handler = {}; // 对象的拦截器
let proxy = new Proxy(target, handler); // 创建Proxy实例

上述代码中,通过创建Proxy实例来拦截target对象的访问。handler是一个拦截器对象,代表对target对象进行拦截和处理。Proxy拦截器需要实现get、set、apply等方法。

2. 常用API:

(1) get:拦截对象属性的读取操作。

let target = {a: 1};
let handler = {
    get(target, property, receiver) {
        console.log(`getting ${property}`);
        return target[property];
    }
};
let proxy = new Proxy(target, handler);
console.log(proxy.a); // 输出getting a 和 1

(2) set:拦截对象属性的赋值操作。

let target = {a: 1};
let handler = {
    set(target, property, value, receiver) {
        console.log(`setting ${property} = ${value}`);
        target[property] = value;
        return true;
    }
};
let proxy = new Proxy(target, handler);
proxy.a = 2; // 输出setting a = 2

(3) apply:拦截函数的调用操作。

let target = function (a, b) {
    return a + b;
}
let handler = {
    apply(target, thisArg, args) {
        console.log(`calling ${target.name}`);
        return target(...args);
    }
};
let proxy = new Proxy(target, handler);
console.log(proxy(1, 2)); // 输出calling proxy 和 3

(4) has:拦截in操作符的操作。
 

let target = {a: 1};
let handler = {
    has(target, property) {
        console.log(`checking ${property} in target`);
        return property in target;
    }
};
let proxy = new Proxy(target, handler);
console.log('a' in proxy); // 输出checking a in target 和 true

(5) ownKeys:拦截Object.getOwnPropertyNames和Object.getOwnPropertySymbols操作。
 

let target = {a: 1};
let handler = {
    ownKeys(target) {
        console.log(`getting ownKeys`);
        return ['b', ...Object.getOwnPropertyNames(target)];
    }
};
let proxy = new Proxy(target, handler);
console.log(Object.keys(proxy)); // 输出getting ownKeys 和 ['b', 'a']

(6) Reflect:Reflect对象提供了Proxy内置拦截函数的基础实现,可以通过Reflect对象执行默认操作。

let target = {a: 1};
let handler = {
    ownKeys(target) {
        console.log(`getting ownKeys`);
        return ['b', ...Object.getOwnPropertyNames(target)];
    }
};
let proxy = new Proxy(target, handler);
console.log(Object.keys(proxy)); // 输出getting ownKeys 和 ['b', 'a']

使用 Proxy 进行数据劫持

Proxy可以用于实现数据劫持,从而实现双向绑定。下面是一种简单的实现方式,可以通过Proxy监听对象的变化并将变化反映到页面上。

HTML代码:

<body>
  <input id="name" type="text">
  <p>你好,<span id="greeting"></span>!</p>
  <script src="app.js"></script>
</body>

JavaScript代码:

let data = {
  name: '',
  greeting: ''
};

let handler = {
  set(target, key, value) {
    target[key] = value;
    document.querySelector(`#${key}`).innerText = value;
    return true;
  }
};

let proxy = new Proxy(data, handler);

// 在input元素中输入文字,会自动更新到greeting元素中
document.querySelector('#name').addEventListener('input', (event)=> {
  proxy.name = event.target.value;
  proxy.greeting = `欢迎,${event.target.value}!`;
});

上面的代码中,我们定义了一个名为data的对象,并通过Proxy来监听它的属性变化。在handler的set拦截方法中,我们将新的属性值写入target对象,并将更新后的属性值实时同步到greeting元素中。在input元素中输入文字时,我们通过Proxy修改name属性的值,并根据修改后的name属性值动态生成greeting属性的值。

Proxy可以深度监听对象的变化,实现对嵌套对象的深度监听。下面是一个简单的实现方式,可以通过Proxy监听对象的变化并输出变化日志。

let data = {
  name: 'Jack',
  age: 29,
  address: {
    city: 'Shanghai',
    district: 'Pudong New Area'
  }
};

let logHandler = {
  get(target, key, receiver) {
    console.log(`getting ${key}: ${JSON.stringify(target[key])}`);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log(`setting ${key}: ${JSON.stringify(value)}`);
    return Reflect.set(target, key, value, receiver);
  }
};

let deepProxy = function (obj) {
  if (typeof obj === 'object' && obj !== null) {
    for (let key in obj) {
      obj[key] = deepProxy(obj[key]);
    }
    return new Proxy(obj, logHandler);
  } else {
    return obj;
  }
}

let proxyData = deepProxy(data);

// 改变对象的属性值,触发日志输出
proxyData.name = 'John';
proxyData.age = 30;
proxyData.address.city = 'Beijing';
proxyData.address.district = 'Haidian District';

在上面的例子中,我们定义了一个名为data的对象,并通过deepProxy函数来深度处理这个对象,返回一个深层次的Proxy代理对象proxyData。在logHandler的get和set方法中,我们输出当前正在访问(或者修改)的属性的名称和值。在最后的代码中,我们修改了proxyData对象的属性值,会触发日志输出。

Proxy可以用于实现表单校验功能,通过代理对象对表单数据进行监听和校验,从而实现表单的数据合法性校验。下面是一个简单的实现方式,可以通过Proxy进行表单数据校验。

HTML代码:

<body>
  <form id="form">
    <label for="username">用户名:</label>
    <input type="text" id="username" name="username" required minlength="5" maxlength="10">
    <span class="error-message"></span>
    <br>
    <label for="password">密码:</label>
    <input type="password" id="password" name="password" required minlength="6">
    <span class="error-message"></span>
    <br>
    <button type="submit">提交</button>
  </form>
  <script src="app.js"></script>
</body>

JavaScript代码:

let form = document.querySelector('#form');

let rules = {
  username: {
    minlength: 5,
    maxlength: 10,
  },
  password: {
    minlength: 6,
  }
};

let handler = {
  set(target, key, value) {
    target[key] = value;

    let field = form.querySelector(`[name="${key}"]`);
    let error = field.nextElementSibling;

    let valid = true;
    for (let rule in rules[key]) {
      if (key !== 'confirm_password' && !validator[rule](value, rules[key][rule], form)) {
        error.innerHTML = validator.errorMessages[rule](key, rules[key][rule]);
        valid = false;
        break;
      }
    }
    if (valid) {
      error.innerHTML = '';
    }

    return true;
  }
};

let validator = {
  minlength(value, length) {
    return value.length >= length;
  },
  maxlength(value, length) {
    return value.length <= length;
  },
  required(value) {
    return value !== '';
  },
  password(value, length) {
    return /[a-z]+/.test(value) && /[A-Z]+/.test(value) && /\d+/.test(value) && value.length >= length;
  },
  confirm_password(value) {
    let password = form.querySelector('[name="password"]').value;
    return value === password;
  },
  errorMessages: {
    minlength(fieldName, value) {
      return `${fieldName}长度不能小于${value}。`;
    },
    maxlength(fieldName, value) {
      return `${fieldName}长度不能大于${value}。`;
    },
    required(fieldName) {
      return `${fieldName}不能为空。`;
    },
    password() {
      return `密码必须包含至少一个大写字母、一个小写字母和一个数字。`;
    },
    confirm_password() {
      return `两次输入的密码不一致。`;
    }
  }
};

let proxy = new Proxy({}, handler);
for (let field of form.elements) {
  if (field.nodeName === 'INPUT') {
    proxy[field.name] = field.value;

    field.addEventListener('input', (event) => {
      proxy[field.name] = event.target.value;
      event.target.nextElementSibling.innerHTML = '';
      event.preventDefault();
    });
  }
}

form.addEventListener('submit', (event) => {
  if (!event.target.checkValidity()) {
    let fields = form.elements;
    for (let field of fields) {
      if (!field.validity.valid) {
        field.nextElementSibling.innerText = field.validationMessage;
      }
    }
    event.preventDefault();
  }
});

在上面的代码中,我们定义了一个名为handler的对象,用于处理表单数据的变化。当表单数据变化时,handler的set方法会处理表单数据的校验,如果校验没有通过,会将错误信息输出到DOM元素中。我们还使用了一个validator对象,定义了表单校验规则和错误信息,用于实现表单数据的校验。在最后的代码中,我们在表单中添加了事件监听器,用于在提交表单时检查表单数据是否合法。

用 Proxy 进行权限控制

通过Proxy,我们可以控制对象属性的访问权限,进而实现对象属性的读取和写入的控制。下面是一个简单的实现方式,可以通过Proxy控制对象属性的访问权限。

let secretData = {
  name: 'Jack',
  age: 29,
  salary: 10000,
};

let secureHandler = {
  get(target, key, receiver) {
    if (key === 'salary') {
      console.log('You do not have access to salary');
      return undefined;
    } else {
      return Reflect.get(target, key, receiver);
    }
  },
  set(target, key, value, receiver) {
    if (key === 'salary') {
      console.log('You do not have access to set salary');
      return false;
    } else {
      return Reflect.set(target, key, value, receiver);
    }
  }
};

let secureData = new Proxy(secretData, secureHandler);

console.log(secureData.name); // Jack
console.log(secureData.age); // 29
console.log(secureData.salary); // You do not have access to salary, undefined

secureData.name = 'John';
secureData.age = 30;
secureData.salary = 20000; // You do not have access to set salary, false

console.log(secureData.name); // John
console.log(secureData.age); // 30
console.log(secureData.salary); // You do not have access to salary, undefined

在上面的代码中,我们定义了一个名为secretData的对象,并通过secureHandler来控制该对象属性的访问权限。在get拦截方法中,我们判断了访问的属性是否是salary,如果是则输出错误信息并返回undefined,否则返回属性值。在set拦截方法中,我们判断了要设置的属性是否是salary,如果是则输出错误信息并返回false,否则将属性值设置成功。

Proxy可以用来实现函数的节流(throttle)操作,即控制函数的调用频率,为了减少函数执行的次数和提升性能。下面是一个简单的实现方式,可以通过Proxy实现函数的节流操作。

function throttle(func, delay) {
  let timer = null;
  return new Proxy(function() {
    if (!timer) {
      timer = setTimeout(() => {
        Reflect.apply(func, this, arguments);
        timer = null;
      }, delay);
    }
  }, {
    apply(target, thisArg, args) {
      clearTimeout(timer);
      timer = setTimeout(() => {
        Reflect.apply(target, thisArg, args);
        timer = null;
      }, delay);
    }
  });
}

function handleResize() {
  console.log('Resized');
}

let throttledResize = throttle(handleResize, 1000); // 设置节流时间为1秒
window.addEventListener('resize', throttledResize);

在上述例子中,我们首先定义了一个throttle函数,它接受一个函数和一个时间间隔作为参数,返回一个代理函数。这个代理函数内部使用了setTimeout函数,用于控制函数的执行间隔。

在使用throttle函数时,我们将要进行节流控制的函数handleResize和时间间隔作为参数,创建了一个代理函数throttledResize。在将throttledResize绑定到resize事件上时,每次窗口尺寸变化都会触发函数的调用,但实际上只有等到上一个延时结束后,才会调用新的函数。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值