从对象到原型链:理解 this、构造函数和 new

从对象到原型链:理解 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.sayNameuser2.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 上,所以 p1p2 共享同一个方法。

这就是原型的好处:

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 显式绑定

通过 callapplybind 指定 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

extendssuper 背后仍然和原型链有关。

可以先这样记:

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

applycall 类似,只是参数用数组传。

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

严格模式下,thisundefined

原因:

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 最终指向谁?
相关推荐
乘风gg2 小时前
AI Coding 提效 2 倍是真的吗?到底怎么衡量效果
前端·ai编程·claude
猫不易2 小时前
Webpack 与 Vite:从 Loader / Plugin 到 Rolldown 统一引擎
前端·vite
YHL2 小时前
🎯 Danci —— 用 AI 驱动开发一个全栈英语单词学习平台
前端·后端
平头哥~2 小时前
Day 20 _ 3D 翻卡_perspective 写错人,卡片就穿帮
前端·3d·css3·学习资料
小聪7082 小时前
elpis-core 前端 Webpack 工程化实践
前端
2601_953988072 小时前
Ricon组态实时监控 - 毫秒级数据可视化
前端·物联网·数学建模·信息可视化·架构·前端框架
V158897262013 小时前
从滴滴模式看机器人租赁:撮合型租赁平台开发源码的调度系统设计思路
linux·前端·机器人
szarron3 小时前
国产手持式频谱分析仪选型攻略:TFN RC系列 vs HTOOL SA8T频谱分析仪 专业参数对比(军工/路测/调试全覆盖)
开发语言·前端·状态模式
开开心心就好3 小时前
免费桌签打印工具,支持批量导入名字
前端·javascript·人工智能·docker·jupyter·智能手机·语音识别