从对象到原型链:理解 this、构造函数和 new
前端面试里,原型链、this、构造函数、new 经常一起出现。
它们不是孤立的知识点,而是一条完整的线:
text
对象是怎么创建的?
函数为什么可以当构造函数?
new 到底做了什么?
实例为什么能访问原型上的方法?
this 到底指向谁?
class 和原型链是什么关系?
这篇文章就把这些问题串起来。
1. JS 里的对象
JS 里很多东西本质上都是对象。
js
const user = {
name: 'Alice',
sayName() {
console.log(this.name);
}
};
user.sayName(); // Alice
对象可以保存属性,也可以保存方法。
text
属性:对象上的数据
方法:对象上的函数
但如果每个对象都单独保存一份方法,就会浪费内存。
比如:
js
const user1 = {
name: 'Alice',
sayName() {
console.log(this.name);
}
};
const user2 = {
name: 'Bob',
sayName() {
console.log(this.name);
}
};
user1.sayName 和 user2.sayName 是两个不同的函数。
如果有一万个用户对象,就会创建一万份类似的方法。
这时候原型就出现了。
2. 构造函数是什么
构造函数就是用来创建对象的函数。
按照习惯,构造函数首字母大写。
js
function Person(name, age) {
this.name = name;
this.age = age;
}
const p1 = new Person('Alice', 18);
const p2 = new Person('Bob', 20);
console.log(p1.name); // Alice
console.log(p2.name); // Bob
这里的 Person 就是构造函数。
它的作用是:
text
通过 new 创建对象,并给新对象添加属性。
3. new 到底做了什么
当我们写:
js
const p = new Person('Alice', 18);
new 大概做了四件事:
text
1. 创建一个新对象。
2. 让新对象的隐式原型指向构造函数的 prototype。
3. 执行构造函数,并把 this 指向这个新对象。
4. 如果构造函数返回的是对象,就返回这个对象;否则返回新创建的对象。
可以手写一个简易版 new:
js
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result !== null && (typeof result === 'object' || typeof result === 'function')
? result
: obj;
}
使用:
js
function Person(name) {
this.name = name;
}
const p = myNew(Person, 'Alice');
console.log(p.name); // Alice
所以 new 的关键是:
text
创建对象。
绑定原型。
绑定 this。
返回对象。
4. prototype 是什么
每个函数都有一个 prototype 属性。
js
function Person(name) {
this.name = name;
}
console.log(Person.prototype);
prototype 是一个对象,它通常用来存放实例共享的方法。
js
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};
const p1 = new Person('Alice');
const p2 = new Person('Bob');
p1.sayName(); // Alice
p2.sayName(); // Bob
console.log(p1.sayName === p2.sayName); // true
因为 sayName 放在了 Person.prototype 上,所以 p1 和 p2 共享同一个方法。
这就是原型的好处:
text
实例自己的属性各自保存。
共享的方法放到原型上。
5. proto 是什么
每个对象都有一个隐式原型,可以通过 __proto__ 访问。
js
function Person(name) {
this.name = name;
}
const p = new Person('Alice');
console.log(p.__proto__ === Person.prototype); // true
__proto__ 指向创建它的构造函数的 prototype。
更推荐的写法是:
js
Object.getPrototypeOf(p) === Person.prototype; // true
因为 __proto__ 是历史遗留访问器,不太推荐在正式代码中直接使用。
但面试里经常用它帮助理解。
6. constructor 是什么
默认情况下,原型对象上有一个 constructor 属性,指回构造函数本身。
js
function Person() {}
console.log(Person.prototype.constructor === Person); // true
实例可以通过原型链访问到它:
js
const p = new Person();
console.log(p.constructor === Person); // true
关系是:
text
p -> Person.prototype -> constructor -> Person
注意,如果你重写了整个 prototype,要手动补回 constructor。
js
function Person() {}
Person.prototype = {
sayName() {
console.log('name');
}
};
console.log(Person.prototype.constructor === Person); // false
修复:
js
Person.prototype = {
constructor: Person,
sayName() {
console.log('name');
}
};
7. 原型链是什么
当访问一个对象的属性时,JS 会先在对象自身查找。
如果找不到,就去它的原型上找。
如果原型上还找不到,就继续往原型的原型上找。
这条查找链路就是原型链。
js
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};
const p = new Person('Alice');
p.sayName(); // Alice
p 自己身上没有 sayName,于是去:
text
p.__proto__
也就是:
text
Person.prototype
上面找到了 sayName。
原型链大概是:
text
p -> Person.prototype -> Object.prototype -> null
所以:
js
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
8. 函数和对象的关系
这一块有点绕,但很高频。
js
function Person() {}
Person 是函数,也是对象。
所以它既有:
js
Person.prototype
也有:
js
Person.__proto__
区别是:
text
prototype:函数作为构造函数时,给实例用的原型对象。
__proto__:函数自己作为对象时,它自己的隐式原型。
比如:
js
function Person() {}
console.log(Person.prototype); // 给实例用
console.log(Person.__proto__ === Function.prototype); // true
因为函数本身也是由 Function 创建出来的。
常见关系:
js
function Person() {}
const p = new Person();
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
面试里不用一上来背所有关系,先抓住一句:
text
实例通过 __proto__ 找构造函数的 prototype。
函数本身也是对象,所以函数也有自己的 __proto__。
9. this 是什么
this 是函数执行时自动生成的一个指向。
注意:
text
this 的指向不是看函数在哪里定义。
this 的指向主要看函数怎么调用。
比如:
js
const name = 'global';
const user = {
name: 'Alice',
sayName() {
console.log(this.name);
}
};
user.sayName(); // Alice
因为调用方式是:
js
user.sayName()
所以 this 指向 user。
10. this 的四种绑定规则
10.1 默认绑定
普通函数直接调用,非严格模式下 this 指向全局对象。
浏览器中通常是 window。
js
function fn() {
console.log(this);
}
fn();
严格模式下:
js
'use strict';
function fn() {
console.log(this);
}
fn(); // undefined
10.2 隐式绑定
谁调用函数,this 就指向谁。
js
const user = {
name: 'Alice',
sayName() {
console.log(this.name);
}
};
user.sayName(); // Alice
这里是 user 调用了 sayName,所以 this 指向 user。
10.3 显式绑定
通过 call、apply、bind 指定 this。
js
function sayName(age) {
console.log(this.name, age);
}
const user = { name: 'Alice' };
sayName.call(user, 18); // Alice 18
sayName.apply(user, [18]); // Alice 18
const fn = sayName.bind(user, 18);
fn(); // Alice 18
区别:
text
call:立即调用,参数一个一个传。
apply:立即调用,参数用数组传。
bind:不立即调用,返回一个新函数。
10.4 new 绑定
用 new 调用构造函数时,this 指向新创建的对象。
js
function Person(name) {
this.name = name;
}
const p = new Person('Alice');
console.log(p.name); // Alice
这里 Person 里的 this 指向 p。
11. this 绑定优先级
多个规则同时出现时,有优先级。
text
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
例子:
js
function Person(name) {
this.name = name;
}
const obj = {};
const BoundPerson = Person.bind(obj);
const p = new BoundPerson('Alice');
console.log(obj.name); // undefined
console.log(p.name); // Alice
虽然 bind 绑定了 obj,但是 new 的优先级更高。
所以 this 最终指向新对象 p。
12. 箭头函数的 this
箭头函数没有自己的 this。
它的 this 来自定义时外层作用域。
js
const user = {
name: 'Alice',
sayName() {
const fn = () => {
console.log(this.name);
};
fn();
}
};
user.sayName(); // Alice
这里箭头函数里的 this 来自外层 sayName。
再看一个容易错的:
js
const user = {
name: 'Alice',
sayName: () => {
console.log(this.name);
}
};
user.sayName();
这里不要以为 this 指向 user。
因为 sayName 是箭头函数,它没有自己的 this,它的 this 来自外层作用域。
在浏览器里,外层可能是 window。
所以对象方法一般不要写成箭头函数。
13. this 丢失
看这段:
js
const user = {
name: 'Alice',
sayName() {
console.log(this.name);
}
};
const fn = user.sayName;
fn();
调用 fn() 时,已经不是 user.sayName() 这种调用形式了。
所以 this 不再指向 user。
这叫 this 丢失。
解决:
js
const fn = user.sayName.bind(user);
fn(); // Alice
或者:
js
setTimeout(() => {
user.sayName();
}, 1000);
14. 原型链和 this 怎么联系起来
很多人会误以为:
text
方法在原型上,所以 this 指向原型。
这是错的。
this 看的是调用方式,不看方法存在哪里。
js
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};
const p = new Person('Alice');
p.sayName(); // Alice
虽然 sayName 是在 Person.prototype 上找到的,但调用方式是:
js
p.sayName()
所以 this 指向 p。
原型链负责:
text
属性查找。
this 负责:
text
函数执行时的上下文。
15. instanceof 的原理
instanceof 用来判断一个构造函数的 prototype 是否出现在对象的原型链上。
js
function Person() {}
const p = new Person();
console.log(p instanceof Person); // true
它不是判断 p 是不是由 Person 创建的。
更准确地说:
text
Person.prototype 是否在 p 的原型链上。
手写:
js
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
const prototype = Constructor.prototype;
while (proto !== null) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
16. Object.create 是什么
Object.create(proto) 会创建一个新对象,并让新对象的原型指向 proto。
js
const parent = {
sayHello() {
console.log('hello');
}
};
const child = Object.create(parent);
child.sayHello(); // hello
console.log(Object.getPrototypeOf(child) === parent); // true
这其实就是在手动设置原型链。
手写 new 时:
js
const obj = Object.create(Constructor.prototype);
就是为了让新对象可以访问构造函数原型上的方法。
17. 原型链继承
JS 里的继承,本质上主要是通过原型链实现的。
17.1 原型链继承
js
function Parent() {
this.names = ['Alice', 'Bob'];
}
Parent.prototype.sayName = function () {
console.log('parent');
};
function Child() {}
Child.prototype = new Parent();
const c1 = new Child();
const c2 = new Child();
c1.names.push('Tom');
console.log(c2.names); // ['Alice', 'Bob', 'Tom']
问题:
text
引用类型属性被所有实例共享。
创建子类实例时不能方便地给父类构造函数传参。
17.2 构造函数继承
js
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name);
}
const c = new Child('Alice');
console.log(c.name); // Alice
优点:
text
可以给父类传参。
每个实例都有自己的属性。
问题:
text
只能继承父类构造函数里的属性,不能继承父类原型上的方法。
17.3 组合继承
js
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const c = new Child('Alice', 18);
c.sayName(); // Alice
问题:
text
父类构造函数会被调用两次。
一次是 Child.prototype = new Parent()。
一次是 Parent.call(this, name)。
17.4 寄生组合继承
这是比较推荐的 ES5 继承方式。
js
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const c = new Child('Alice', 18);
c.sayName(); // Alice
优点:
text
可以继承实例属性。
可以继承原型方法。
不会重复调用父类构造函数。
18. class 和原型链
ES6 的 class 本质上还是基于原型链。
js
class Person {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
const p = new Person('Alice');
p.sayName(); // Alice
这里的:
js
sayName() {}
其实是放在:
js
Person.prototype
上的。
所以:
js
console.log(p.__proto__ === Person.prototype); // true
console.log(p.sayName === Person.prototype.sayName); // true
class 的继承:
js
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name);
}
}
class Dog extends Animal {
constructor(name, age) {
super(name);
this.age = age;
}
}
const dog = new Dog('Lucky', 3);
dog.speak(); // Lucky
extends 和 super 背后仍然和原型链有关。
可以先这样记:
text
class 是原型链继承的语法糖。
19. 手写 call
call 的作用是指定函数执行时的 this,并立即调用函数。
js
Function.prototype.myCall = function (context, ...args) {
context = context === null || context === undefined ? globalThis : Object(context);
const key = Symbol('fn');
context[key] = this;
const result = context[key](...args);
delete context[key];
return result;
};
使用:
js
function say(age) {
console.log(this.name, age);
}
say.myCall({ name: 'Alice' }, 18); // Alice 18
思路:
text
把函数临时挂到对象上。
通过对象调用函数。
调用完删除临时属性。
因为:
js
obj.fn()
这种调用方式会让 this 指向 obj。
20. 手写 apply
apply 和 call 类似,只是参数用数组传。
js
Function.prototype.myApply = function (context, args) {
context = context === null || context === undefined ? globalThis : Object(context);
args = args || [];
const key = Symbol('fn');
context[key] = this;
const result = context[key](...args);
delete context[key];
return result;
};
使用:
js
function say(age, city) {
console.log(this.name, age, city);
}
say.myApply({ name: 'Alice' }, [18, 'Shanghai']); // Alice 18 Shanghai
21. 手写 bind
bind 不会立即调用函数,而是返回一个新函数。
js
Function.prototype.myBind = function (context, ...bindArgs) {
const fn = this;
function boundFn(...callArgs) {
const isNew = this instanceof boundFn;
const finalThis = isNew ? this : context;
return fn.apply(finalThis, bindArgs.concat(callArgs));
}
boundFn.prototype = Object.create(fn.prototype);
return boundFn;
};
使用:
js
function say(age, city) {
console.log(this.name, age, city);
}
const fn = say.myBind({ name: 'Alice' }, 18);
fn('Shanghai'); // Alice 18 Shanghai
这里还处理了 new 调用:
js
const BoundPerson = Person.myBind(obj);
const p = new BoundPerson();
如果绑定后的函数被 new 调用,this 应该指向新对象,而不是原来绑定的 obj。
22. 常见面试输出题
22.1 原型方法里的 this
js
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};
const p = new Person('Alice');
p.sayName();
输出:
text
Alice
原因:
text
方法虽然在原型上,但调用方式是 p.sayName(),所以 this 指向 p。
22.2 this 丢失
js
const user = {
name: 'Alice',
sayName() {
console.log(this.name);
}
};
const fn = user.sayName;
fn();
非严格模式浏览器环境下,this 可能指向 window。
严格模式下,this 是 undefined。
原因:
text
fn() 是普通函数调用,不再是 user.sayName(),所以 this 丢失。
22.3 箭头函数 this
js
const user = {
name: 'Alice',
sayName: () => {
console.log(this.name);
}
};
user.sayName();
这里 this 不指向 user。
原因:
text
箭头函数没有自己的 this,它的 this 来自定义时的外层作用域。
23. 面试怎么答
如果面试官问:
text
说一下原型链。
可以这样答:
text
每个对象都有隐式原型,函数有 prototype 属性。通过 new 创建实例时,实例的隐式原型会指向构造函数的 prototype。当访问对象属性时,会先找对象自身,如果找不到,就沿着隐式原型继续向上查找,直到 Object.prototype,最后到 null。这条查找链路就是原型链。
如果面试官问:
text
new 做了什么?
可以这样答:
text
new 会创建一个新对象,让新对象的隐式原型指向构造函数的 prototype,然后执行构造函数,并把 this 指向这个新对象。最后如果构造函数返回的是对象,就返回这个对象,否则返回新创建的对象。
如果面试官问:
text
this 指向怎么判断?
可以这样答:
text
this 的指向主要看函数调用方式。普通函数调用是默认绑定,非严格模式指向全局对象,严格模式是 undefined;对象方法调用是隐式绑定,this 指向调用者;call、apply、bind 是显式绑定;new 调用时 this 指向新创建的对象。箭头函数没有自己的 this,它的 this 来自定义时外层作用域。
如果面试官问:
text
class 和原型链什么关系?
可以这样答:
text
class 本质上还是基于原型链的语法糖。class 中定义的普通方法会放到构造函数的 prototype 上,实例通过原型链访问这些方法。extends 背后也是建立子类和父类之间的原型关系。
24. 最后总结
这一组知识点可以这样串:
text
构造函数负责初始化对象。
new 负责创建对象、绑定原型、绑定 this。
prototype 存放实例共享的方法。
实例通过 __proto__ 连接到构造函数的 prototype。
属性查找沿着原型链向上找。
this 不看方法在哪定义,主要看函数怎么调用。
class 是原型链的语法糖。
做题时可以按这个顺序分析:
text
1. 这个对象是谁创建的?
2. 它的 __proto__ 指向谁?
3. 属性或方法是在自身还是原型上?
4. 函数是怎么被调用的?
5. this 最终指向谁?