this:调用方式决定普通函数的接收者
上一节的变量沿定义位置查找。普通函数的 this 却通常在调用时决定。两套机制可以同时存在于同一个函数中,不能互相代替。
对象方法是存储在属性中的函数
const reader = {
name: '小林',
introduce() {
return this.name;
},
};
console.log(reader.introduce()); // 小林introduce 是一个函数属性。通过 reader.introduce() 调用时,reader 作为接收者传给普通函数的 this。对象字面量本身不创建供箭头函数读取的 this 作用域。
const reader = {
name: '小林',
identify() { return this; },
};
const detached = reader.identify;
console.log(reader.identify() === reader); // true
console.log(detached()); // undefined,在本书严格模式下取出函数值再独立调用,属性访问形成的接收者关系就丢失了。函数并没有永远绑定“最初存放它的对象”。把对象方法作为回调传出去时尤其容易发生这种情况。
call、apply 立即调用,bind 产生新函数
function title(prefix, suffix) {
return `${prefix}${this.name}${suffix}`;
}
const chapter = { name: '函数' };
console.log(title.call(chapter, '《', '》')); // 《函数》
console.log(title.apply(chapter, ['[', ']'])); // [函数]
const bound = title.bind(chapter, '学习:');
console.log(bound('!')); // 学习:函数!call 后面逐个传参数;apply 接收类数组参数列表;bind 保存接收者与部分前置参数,稍后调用。绑定函数再次 .call(other) 不会替换已经绑定的 this。它们改变的是调用契约,不会改写函数定义位置的词法作用域。
严格普通函数独立调用时 this 是 undefined;非严格普通函数会把空接收者替换为全局对象,并包装原始值。浏览器普通脚本、ES 模块、Node CommonJS 的顶层 this 又各有差异,因此示例必须声明环境。
先模拟普通调用,才知道 bind 多保存了什么
不修改内建原型,只写一个用途受限的普通调用包装器:
function keepReceiver(fn, receiver, ...prefixArgs) {
return function (...laterArgs) {
return fn.call(receiver, ...prefixArgs, ...laterArgs);
};
}
function label(prefix, suffix) {
return `${prefix}${this.name}${suffix}`;
}
const saved = keepReceiver(label, { name: '作用域' }, '阅读:');
console.log(saved('。')); // 阅读:作用域。外层调用保存函数、接收者和前置参数,内层调用接收剩余参数,再把目标函数结果返回。少了最内层return,包装器就会吞掉结果;少了接收者,严格普通函数独立调用时就得到undefined;把前置参数放到后面,则改变接口顺序。把这三个部分分别检查,比记“bind返回新函数”更能解释行为。
这只是普通调用的教学模型,不是完整bind替代实现。真正绑定函数的构造行为、name/length、原型及内部目标记录有专门规则,不能由几行call包装自动实现。
此外,每次 method.bind(obj) 都产生新函数。需要以后移除同一个回调时,应保存绑定结果并复用;重新bind一次的函数身份不同,虽然它的行为看起来一样。
箭头函数从外层取得 this
const reader = {
name: '小林',
makeIntroduction() {
return () => this.name;
},
};
const introduce = reader.makeIntroduction();
console.log(introduce()); // 小林
console.log(introduce.call({ name: '小周' })); // 小林箭头在普通方法执行期间创建,读取该次方法调用的 this。随后箭头被取出也不会改为新的接收者。箭头没有自己的 arguments、super、new.target 绑定;可从允许的外层环境读取相应内容。它不能通过 new 构造对象。
写 const obj = { show: () => this } 时,箭头的外层不是 obj;在模块顶层得到的是 undefined。需要方法接收者就写普通方法,需要保留外层接收者就考虑箭头。
new 建立新对象并以它作为 this
function Bookmark(page) {
this.page = page;
}
const mark = new Bookmark(12);
console.log(mark.page); // 12概念步骤是:创建一个对象,将它连接到构造函数的 prototype,用新对象作 this 执行构造函数,再决定返回值。构造函数显式返回对象时替代新对象;返回原始值一般不替代。原型链在第四章完整展开,这里只解释调用方式的影响。
绑定函数若背后的目标可构造,也可以被 new 调用;这时忽略绑定的 this,仍保留绑定参数。不能把“bind 优先级最高”当成不带前提的口诀。
new 的“忽略绑定接收者”可以直接观察
function Bookmark(page) {
this.page = page;
}
const existing = { page: 0 };
const BoundBookmark = Bookmark.bind(existing, 12);
const created = new BoundBookmark();
console.log(existing.page, created.page); // 0 12
console.log(created === existing); // false
console.log(created instanceof Bookmark); // true前置参数12保留了,但existing并没有被写入。构造调用要初始化新对象,不能再把普通调用时绑定的接收者拿来充当新对象;因此“bind的this优先级最高”只适用于缺少构造分支的简化口诀。
再区分普通调用与构造调用的显式返回值:
function build() {
this.page = 1;
return { page: 9 };
}
const receiver = {};
console.log(build.call(receiver).page, receiver.page); // 9 1
console.log(new build().page); // 9普通调用把返回对象交给调用者,同时receiver已经被修改;构造调用则让显式返回对象替代默认的新对象。若构造函数返回原始值,普通函数构造通常忽略这个原始返回值。类的派生构造器有更细的限制,第四章讨论,不把此处普通函数规律随意扩大。
同一个函数怎样判断接收者
先判断是否为箭头;是则找外层绑定。普通函数再看是否 new 构造、是否绑定函数、是否显式 call/apply、是否通过对象属性调用,最后才是独立调用规则。
括号本身不一定丢接收者,(obj.method)() 仍保留;赋值、逗号、逻辑运算等取到单独函数值后再调用,可能丢失。背“看点号左边”只适合简单表达式,复杂情况应把取值与调用拆成两行。
练习
将
reader.introduce交给另一个变量,怎样让它稍后仍返回名字?分别给出bind和包装函数方案。为什么对象字面量中的箭头方法读不到该对象的
name,而普通方法返回的箭头可以?普通调用包装器为什么必须return目标函数的结果?new调用绑定构造函数时,绑定接收者和前置参数各会怎样处理?