JavaScript非继承实现类实例方法属性自动代理方案咨询
实现类实例自动代理内部对象的方法与属性(无需继承)
我是JavaScript新手,希望在不使用继承的前提下,让类实例自动代理其内部持有的另一个类实例的方法与属性。比如下面的示例代码中,我期望B的实例能直接调用z实例的方法和访问属性,不用逐个手动封装:
class Z{ constructor(){} a(){console.log('a')} get b(){console.log('b')} c(){console.log('c')} } class B{ constructor(z){ this.z = z; } } let z = new Z(); let b = new B(z); b.a(); // 当前会报错,期望能直接调用z的a方法 b.b; // 当前会报错,期望能直接访问z的b属性
实际业务场景中,我封装了CodeEditor类来包装CodeMirror实例(CodeMirror实例由库的fromTextArea方法生成,无法直接用extends继承),现在需要手动重写CodeMirror的方法,但项目其他代码会调用CodeMirror的原型方法,希望有通用的自动代理方案,避免逐个重写。
方案1:使用ES6 Proxy实现自动代理
Proxy可以拦截对象的属性访问、方法调用等操作,完美适配这种自动代理需求。在类的构造函数中返回Proxy实例,即可拦截对当前实例的所有属性访问,优先转发给内部目标对象。
适配Z/B场景的示例代码:
class Z{ constructor(){} a(){console.log('a')} get b(){console.log('b')} c(){console.log('c')} } class B{ constructor(z){ this.z = z; // 返回Proxy实例,拦截属性访问 return new Proxy(this, { get(target, prop) { // 优先返回自身属性/方法 if (prop in target) { return Reflect.get(target, prop); } // 转发给内部z实例,绑定this避免调用错误 if (prop in target.z) { const value = Reflect.get(target.z, prop); return typeof value === 'function' ? value.bind(target.z) : value; } return undefined; }, // 可选:拦截属性设置操作,转发给z实例 set(target, prop, value) { if (prop in target.z) { return Reflect.set(target.z, prop, value); } return Reflect.set(target, prop, value); } }); } } let z = new Z(); let b = new B(z); b.a(); // 输出'a' b.b; // 输出'b' b.c(); // 输出'c'
适配CodeEditor场景的修改:
修改CodeEditor构造函数,返回Proxy实例自动代理内部的this.editor:
class CodeEditor{ constructor( container, generic, types, modes, options, ){ this.generic = generic; this.types = types; this.modes = modes; this.container = container; this.textarea = container.querySelector('textarea'); this.header = this.container.querySelector('.cm-header-bar'); this.compileBtnWrapper = this.header.querySelector('.cm-code-compile-wrapper'); this.compileBtn = document.createElement('div'); this.compileBtn.classList.add('cm-code-compile'); this.compileBtn.innerText = 'compile'; this.options = options; this.mode = this.textarea.getAttribute('id'); this.options.mode.name = this.mode; this.editor = CodeMirror.fromTextArea(this.textarea, this.options); // editor setup this.editor.on("gutterClick", function(cm, n) { let info = cm.lineInfo(n); cm.setGutterMarker(n, "breakpoints", info.gutterMarkers ? null : makeMarker()); }); // compilable if(this.mode !== this.generic)this.compilable(); this.dropdown = dropdown(this.header.querySelector('.dropdown-button'), '预先处理', generic); Object.keys(this.modes).forEach(mode=>{ this.dropdown.addOption(mode, ()=>{ this.setOption('mode', { name: this.name, globalVars: true }); this.mode = mode; this.editor.refresh(); play(); if(mode !== this.generic)this.compilable(); }, mode === this.mode); }); // 返回Proxy实例,自动代理editor的属性和方法 return new Proxy(this, { get(target, prop) { // 优先返回自身属性/方法 if (prop in target) { return Reflect.get(target, prop); } // 转发给editor实例,绑定this为editor if (prop in target.editor) { const value = Reflect.get(target.editor, prop); return typeof value === 'function' ? value.bind(target.editor) : value; } return undefined; }, set(target, prop, value) { if (prop in target.editor) { return Reflect.set(target.editor, prop, value); } return Reflect.set(target, prop, value); } }); } get name(){ return this.types[this.mode]; } compilable(){ this.compileBtnWrapper.appendChild(this.compileBtn); let temp = {}; const oxidize = () => { this.compileBtn.onclick = () => { temp.code = this.getValue(); temp.mode = this.mode ; this.dropdown.onOption(this.generic); this.setValue(this.modes[this.mode]()); this.options.mode.name = this.types[this.generic]; this.setOption('mode', this.options.mode); this.compileBtn.classList.add('active'); this.compileBtn.innerText = 'restore'; reduce(); } } const reduce = () => { this.compileBtn.onclick = () => { this.dropdown.onOption(temp.mode); this.setValue(temp.code); this.options.mode.name = temp.mode; this.setOption('mode', this.types[temp.mode]); this.compileBtn.classList.remove('active'); this.compileBtn.innerText = 'compile'; oxidize(); } } oxidize(); } // 移除手动封装的setValue、getValue等方法,Proxy会自动转发 compiled(){return this.modes[this.mode]() || ''}; raw(){return this.getValue().trimEnd()} }
修改后,所有对CodeEditor实例的属性/方法访问,只要自身未定义,都会自动转发给内部的editor实例,无需逐个手动封装。
方案2:使用Object.defineProperty实现属性转发(兼容旧环境)
如果需要兼容不支持Proxy的旧浏览器,可以遍历目标对象的属性,通过Object.defineProperty动态添加到当前实例:
class B{ constructor(z){ this.z = z; // 遍历z的自身属性和原型链属性 const proto = Object.getPrototypeOf(z); const allProps = [...Object.getOwnPropertyNames(z), ...Object.getOwnPropertyNames(proto)]; allProps.forEach(prop => { if (prop in this) return; Object.defineProperty(this, prop, { get() { const value = z[prop]; return typeof value === 'function' ? value.bind(z) : value; }, set(value) { z[prop] = value; }, enumerable: true }); }); } }
这种方法的局限性是只能初始化时转发已存在的属性,无法代理目标对象后续新增的属性,而Proxy可以动态拦截所有属性访问。
内容的提问来源于stack exchange,提问作者Weilory
相关产品推荐
相关产品推荐

