DrillLab
第 05 / 25 节LESSON 05 / 25约 16 分钟~16 min

this 与面向对象三问3 questions on this and object-oriented programming

OOP、this 指向的四条规则、call/apply/bind。OOP, the four rules for what this points to, call/apply/bind.

面试 · 第 3 部分Interview · Part 3
这一页有什么On this page4
学完这节你会After this lesson you can
  • 按优先级说出 this 指向的四条判定规则State the four rules that decide what this points to, in priority order
  • 分清 call、apply、bind 三者的差别并手写一个 bindTell call, apply and bind apart, and write your own bind
  • 说明 JS 的原型继承和 class 的关系Explain how prototype inheritance in JavaScript relates to class
这在考试里考什么What the exam does with this

this 是「给你一段代码问输出什么」的常客,而且答错就说明基本功不牢。手写 bind、手写 new、手写继承是现场编码题的高频三件套。这一组也是理解 React 类组件为什么要 bind 的前提。this shows up whenever you are handed code and asked what it prints, and a wrong answer says the basics are not solid. Writing bind, writing new, and writing inheritance by hand are three frequent live coding tasks. This group is also what you need before you can explain why a React class component calls bind.

§01

什么是面向对象编程What is object-oriented programming?

#302 What is Object-Oriented Programming (OOP)

一句话:把数据和操作数据的方法打包在一起,用对象来组织程序。

四个特征(背下来):

  • 封装—— 内部细节藏起来, 只暴露必要的接口。JS 里用闭包或#private 字段实现。
  • 继承—— 子类复用父类的能力。
  • 多态—— 同一个方法名, 不同对象有不同行为。
  • 抽象—— 只关心「能做什么」, 不关心「怎么做的」。

JS 的特别之处(这才是考点):它是基于原型(prototype)的, 不是基于类的。class 是 ES6 加的语法糖, 底下还是原型链 ——class A extends B 编译后就是设置A.prototype.__proto__ = B.prototype

原型链一句话:访问一个属性时,对象自己没有就去__proto__ 上找, 一层层往上直到 null和作用域链是一个套路, 只是一个查变量、一个查属性。

会追问:「React 为什么从 class 转向函数组件?」—— 因为 UI 更适合用「输入 → 输出」来描述, 而不是「一个有生命周期的对象」; 而且 class 里 this的绑定问题、逻辑按生命周期而不是按关注点拆分, 都是实际痛点(见 #322)。

In one line: bundle data together with the methods that act on it, and organise the program around objects.

Four pillars — memorise these:

  • Encapsulation — hide the internals, expose only the interface callers need. In JS you get it from closures or #private fields.
  • Inheritance — a subclass reuses what the parent can already do.
  • Polymorphism — same method name, different behaviour per object.
  • Abstraction — you care what it can do, not how it does it.

What makes JS different, and this is the real question: it is prototype-based, not class-based. class is syntax sugar added in ES6; the prototype chain is still underneath — class A extends B compiles down to setting A.prototype.__proto__ = B.prototype.

The prototype chain in one line: read a property, and if the object does not have it the lookup walks up __proto__ one level at a time until null. Same idea as the scope chain — one looks up variables, the other looks up properties.

Follow-up: “Why did React move from classes to function components?” — because UI is easier to describe as “input → output” than as an object with a lifecycle. On top of that, this binding and code split by lifecycle instead of by concern were real, daily pain (see #322).

JavaScript四个特征与原型链The four traits and the prototype chain示意Illustrative
1class Animal {
2 #secret = "私有字段,外部访问不到"; // 封装
3 constructor(name) { this.name = name; }
4 speak() { return `${this.name} 发出声音`; }
5}
6
7class Dog extends Animal {
8 speak() { return `${this.name} 汪汪`; } // 多态:覆盖父类方法
9}
10
11new Dog("旺财").speak(); // "旺财 汪汪"
12
13// class 只是语法糖,底下是原型链
14Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
15new Dog("x") instanceof Animal; // true
1class Animal {
2 #secret = "a private field, unreachable from outside"; // encapsulation
3 constructor(name) { this.name = name; }
4 speak() { return `${this.name} makes a sound`; } // encapsulation
5}
6
7class Dog extends Animal {
8 speak() { return `${this.name} woof`; } // polymorphism: overrides the parent
9}
10
11new Dog("旺财").speak(); // the name is data, so it stays: <name> woof
12
13// class is only syntax sugar; a prototype chain sits underneath
14Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
15new Dog("x") instanceof Animal; // true
§02

this 指向什么What does this refer to?

#303 What does 'this' refer to

一句话:this调用时决定的, 不是定义时。看「谁调用的」

四条规则,按优先级从高到低—— 这个顺序是标准答案:

  1. new 绑定——new Foo()this 是新创建的对象。
  2. 显式绑定——call / apply /bind 指定的那个。
  3. 隐式绑定——obj.fn()thisobj看点号左边)。
  4. 默认绑定—— 都不满足时, 严格模式下是 undefined, 否则是 window / global

箭头函数是例外,它不参与这四条—— 它没有自己的 this, 用的是定义时外层作用域的 this, 而且 call / bind改不了它

最经典的坑:隐式丢失。const fn = obj.method 之后单独调fn(),点号没了,this 就丢了。 把方法当回调传出去(setTimeout(obj.method)onClick={this.handle}) 都是这个问题 ——这就是 React 类组件必须在构造器里bind 的原因

会追问:「DOM 事件回调里 this 是什么?」—— 普通函数是绑定事件的那个元素(等于 e.currentTarget), 箭头函数则是外层的 this

In one line: this is decided when the function is called, not where it was written. Look at who called it.

Four rules, highest priority first — this order is the model answer:

  1. new binding — with new Foo(), this is the object that was just created.
  2. Explicit binding — whatever you handed to call / apply / bind.
  3. Implicit bindingobj.fn() makes this the obj (look left of the dot).
  4. Default binding — when none of the above applies: undefined in strict mode, otherwise window / global.

Arrow functions are the exception; they do not play by those four rules — an arrow has no this of its own, it uses the this of the scope it was defined in, and call / bind cannot change that.

The classic trap: the implicit binding gets lost. Do const fn = obj.method and then call fn() on its own — the dot is gone, so this is gone. Handing a method off as a callback (setTimeout(obj.method), onClick={this.handle}) is the same bug — and it is exactly why React class components had to bind in the constructor.

Follow-up: “What is this inside a DOM event handler?” — in a normal function it is the element the listener is attached to (the same as e.currentTarget); in an arrow function it is the outer this.

JavaScript四条规则与隐式丢失The four rules, and losing the implicit binding示意Illustrative
1const obj = {
2 name: "obj",
3 show() { console.log(this.name); },
4};
5
6obj.show(); // "obj" 隐式绑定,看点号左边
7const f = obj.show;
8f(); // undefined 隐式丢失
9f.call({ name: "call" }); // "call" 显式绑定
10setTimeout(obj.show, 0); // undefined 传出去就丢了
11setTimeout(() => obj.show(), 0); // "obj" 包一层就保住了
12
13// 箭头函数不参与规则,call 也改不了
14const arrow = () => console.log(this);
15arrow.call({ a: 1 }); // 还是外层的 this
16
17// React 类组件为什么要 bind
18class Btn extends React.Component {
19 constructor(p) {
20 super(p);
21 this.handle = this.handle.bind(this); // 不 bind,onClick 里 this 就是 undefined
22 }
23 handle() { console.log(this.props); }
24}
1const obj = {
2 name: "obj",
3 show() { console.log(this.name); },
4};
5
6obj.show(); // "obj" implicit binding: look left of the dot
7const f = obj.show;
8f(); // undefined the implicit binding is lost
9f.call({ name: "call" }); // "call" explicit binding
10setTimeout(obj.show, 0); // undefined pass it out and the binding is gone
11setTimeout(() => obj.show(), 0); // "obj" one wrapper keeps it
12
13// An arrow function ignores these rules, and call cannot change it either
14const arrow = () => console.log(this);
15arrow.call({ a: 1 }); // still the outer this
16
17// Why a React class component needs bind
18class Btn extends React.Component {
19 constructor(p) {
20 super(p);
21 this.handle = this.handle.bind(this); // without bind, this is undefined in onClick
22 }
23 handle() { console.log(this.props); }
24}
§03

call、apply、bind 的区别What is the difference between call, apply and bind?

#304 What are the differences between call, apply & bind

一句话:三个都是改thiscallapply立即执行,bind 返回一个新函数call 参数一个个传,apply 传数组。

是否立即执行参数形式
fn.call(ctx, a, b)立即逐个(Comma)
fn.apply(ctx, [a, b])立即数组(Array)
fn.bind(ctx, a)不执行,返回新函数逐个,且可以只绑一部分

记法:Apply 收 Array,Call 用 Comma。

bind 的两个额外性质(追问点):

  • 能预置参数—— 所以它就是偏函数 (见 #299)。
  • 绑过一次就锁死了—— 再 bindcall都改不回来。new 能突破它, 因为 new 优先级最高。

还会追问:apply 现在还有用吗?」—— 展开语法出来后大部分被fn(...args) 取代了。但转发不定参数时还常用fn.apply(this, args)(防抖里就是这么写的,因为要同时转发thisargs)。

In one line: all three change this. call and apply run the function right away, bind hands you back a new one; call takes its arguments one by one, apply takes an array.

Runs immediately?Argument form
fn.call(ctx, a, b)YesOne by one (Comma)
fn.apply(ctx, [a, b])YesAn array (Array)
fn.bind(ctx, a)No — you get a new functionOne by one, and you may bind only some of them

How to remember it: Apply takes an Array, Call takes Commas.

Two extra properties of bind they will probe:

  • It can preset arguments — which makes it partial application (see #299).
  • Bind once and it is locked — another bind or a call cannot change it back. But new breaks through, because new has the highest priority.

Another follow-up: “Is apply still useful?” — spread syntax replaced most of it with fn(...args). It is still the normal way to forward an unknown argument list: fn.apply(this, args) — that is how debounce is written, because you have to forward this and args at the same time.

JavaScript三者对比与手写 bindThe three compared, and bind written by hand示意Illustrative
1function greet(greeting, mark) {
2 return `${greeting}, ${this.name}${mark}`;
3}
4const who = { name: "小明" };
5
6greet.call(who, "你好", "!"); // "你好, 小明!"
7greet.apply(who, ["你好", "!"]); // 同上,参数是数组
8const hi = greet.bind(who, "你好"); // 返回新函数,还预置了第一个参数
9hi("?"); // "你好, 小明?"
10
11// 手写一个 bind(高频现场题)
12Function.prototype.myBind = function (ctx, ...preset) {
13 const fn = this;
14 return function bound(...args) {
15 // new 调用时 this 是新对象,此时不该用 ctx —— 这是 bind 的规范行为
16 const isNew = this instanceof bound;
17 return fn.apply(isNew ? this : ctx, [...preset, ...args]);
18 };
19};
1function greet(greeting, mark) {
2 return `${greeting}, ${this.name}${mark}`;
3}
4const who = { name: "小明" };
5
6greet.call(who, "你好", "!"); // the greeting, then the name, then the mark
7greet.apply(who, ["你好", "!"]); // the same, but the arguments arrive as an array
8const hi = greet.bind(who, "你好"); // returns a new function with the first argument preset
9hi("?"); // the same string, ending in a question mark
10
11// Write bind by hand (a very common live-coding question)
12Function.prototype.myBind = function (ctx, ...preset) {
13 const fn = this;
14 return function bound(...args) {
15 // With new, this is the new object and ctx must be ignored —— the spec requires it
16 const isNew = this instanceof bound;
17 return fn.apply(isNew ? this : ctx, [...preset, ...args]);
18 };
19};
手写 bind 的加分项就是那个 isNew 判断:规范规定 new 一个 bound 函数时,绑定的 this 应该被忽略。多数人会漏。What earns extra credit when you write bind by hand is that isNew check: the spec says that when a bound function is called with new, the bound this must be ignored. Most people leave it out.
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

「this 是 undefined」this is undefined
隐式丢失 —— 点号没了;用 bind 或箭头包一层The implicit binding was lost — the dot is gone; use bind, or wrap it in an arrow function
给你代码问 this 是什么Handed code and asked what this is
按 new > 显式 > 隐式 > 默认 四条走;箭头看外层Work through the four rules in order: new > explicit > implicit > default; for an arrow function look at the enclosing scope
要转发不定参数和 thisYou need to forward this and an unknown number of arguments
fn.apply(this, args)fn.apply(this, args)
问 class 和原型的关系Asked how class relates to prototypes
class 是语法糖,底下是原型链class is syntax sugar; what it builds is a prototype chain
这节的要点What to take away
  1. OOP 四特征:封装、继承、多态、抽象;JS 是原型继承,class 只是语法糖。OOP has four traits: encapsulation, inheritance, polymorphism and abstraction; JavaScript inherits through prototypes, and class is only syntax sugar.
  2. this 四条规则按优先级:new > call/apply/bind > obj.fn() > 默认;箭头函数不参与。The four rules for this, in priority order: new > call/apply/bind > obj.fn() > default; an arrow function follows none of them.
  3. 隐式丢失是最常见的坑,也是 React 类组件要 bind 的原因。Losing the implicit binding is the most common mistake, and it is why a React class component has to call bind.
  4. Apply 收 Array、Call 用 Comma;bind 返回新函数、能预置参数、绑一次锁死但 new 能突破。Apply takes an Array, Call takes Commas; bind returns a new function, can preset arguments, and binds once for good — only new can override it.

接下来What next

  1. 接着看下一节Continue to the next lesson异步与事件循环六问6 questions on async and the event loop
    下一节Next lesson
  2. 可选:再巩固一下Optional: reinforce it这一节的 3 道八股3 questions from this lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 函数与作用域十二问12 questions on functions and scope