普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月17日首页

全面解析this-理解this指向的原理

作者 pepedd864
2025年8月17日 12:03

参考资料

  • 《你不知道的JavaScript》- this全面解析

this 是什么

  • this 是一个代词,代指一个对象
  • this 提供了一种更优雅的方式来隐式的传递一个对象引用,可以让代码更加简洁易于复用

调用位置

调用位置就是函数在代码中被调用的位置(而不是声明的位置)。调用位置决定了 this 的绑定

比如下面代码:

function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log("baz");
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log("bar");
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo // 因此,当前调用位置在 bar 中
console.log("foo");
}
baz(); // <-- baz 的调用位置

可以使用浏览器的开发工具查看调用栈




this的绑定规则

this的指向有以下四条特性/规则

  • 默认绑定
  • 隐式绑定
  • 显式绑定
  • new 绑定

默认绑定

当函数被独立调用时,函数的 this 指向 window

独立调用就是像这样:foo() 的调用

比如

function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2

如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined

隐式绑定

当函数引用有上下文对象 且被该对象调用时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象

function foo() {
console.log(this.a);
}
var obj = {
  a: 2,
  foo: foo
};
obj.foo(); // 2
// 这里就是隐式绑定,foo函数的this绑定到了obj上

需要注意的是,对象属性引用链中只有最顶层或者说最后一层会影响调用位置

比如

function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo,
};
var obj1 = {
a: 2,
obj2: obj2,
};
obj1.obj2.foo(); // 42
// 相当于 obj2.foo(),因为只有最后一层会影响调用位置

隐式丢失

隐式绑定的函数可能会丢失绑定对象,也就是说它会应用默认绑定

隐式丢失的几种情况:

  1. 函数别名
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};

// 注意这里的函数别名,会导致隐式绑定丢失,导致foo函数的this指向全局
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
  1. 函数作为参数传入,并调用时
function foo() {
console.log(this.a);
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo,
};
var a = "oops, global"; // a 是全局对象的属性
doFoo(obj.foo); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样

显式绑定

  • fn.call(obj, x, x, ...) 将fn函数的this指向obj,并调用,call的剩余参数是fn需要的参数
function foo(aram) {
console.log("foo", param.a); // foo 1

function bar(x, y) {
console.log("bar", this.a, x, y); // bar 1 2 3
}
bar.call(param, 2, 3);
}

foo({ a: 1 });
  • fn.apply(obj, [x, x, ...]) 将fn函数的this指向obj,并调用,apply的第二个参数是一个数组
function foo(param) {
console.log("foo", param.a); // foo 1

function bar(x, y) {
console.log("bar", this.a, x, y); // bar 1 2 3
}
bar.apply(param, [2, 3]);
}

foo({ a: 1 });
  • fn.bind(obj, x, x, ...)(x, x, ...) 将fn函数的this指向obj,bind会返回一个新的函数,新的函数也可以传递参数
function foo(param) {
console.log("foo", param.a); // foo 1

function bar(x, y) {
console.log("bar", this.a, x, y); // bar 1 2 3
}
return bar.bind(param, 2);
}

const bar = foo({ a: 1 });
bar(3);

new 绑定

new 绑定 - this会绑定到新创建的对象上

function Person(name, age) {
  // this 指向新创建的对象
  this.name = name;
  this.age = age;

  // 通过new调用构造函数时,this指向新创建的对象
// 直接调用构造函数时,应用默认绑定规则,this指向全局或undefined
  console.log(this);
}

// 使用 new 关键字调用构造函数
const person1 = new Person("张三", 25);
console.log(person1.name); // "张三"
console.log(person1.age);  // 25

// 不使用 new 调用,this 会指向全局对象(非严格模式)或 undefined(严格模式)
const person2 = Person("李四", 30); // this 不会指向新对象
console.log(person2); // undefined (因为没有显式返回)

箭头函数

  1. 箭头函数没有自己的this 指向,它需要继承外层函数的this指向
  2. 箭头函数即使是new也无法改变this指向,因此箭头函数不能用于编写构造函数
var a = 1;
function foo() {
var obj1 = {
a: 2,
bar: function () {
console.log("bar", this.a);
var obj2 = {
a: 3,
baz: () => {
console.log("baz", this.a);
},
};
// 箭头函数不会创建自己的 this,它会捕获外层函数的 this
obj2.baz(); // baz 2
},
};
console.log("foo", this.a);
obj1.bar(); // bar 2
}
foo(); // foo 1

探究js继承实现方式-js面向对象的基础

作者 pepedd864
2025年8月17日 11:00

继承

  • 是什么
    • 继承是面向对象三大特性:继承、封装、多态 之一
    • 具体来说就是让子类可以访问到父类的方法
  • 实现方式有?
    • 原型链继承
    • 构造函数继承
    • 组合继承
    • 原型式继承
    • 寄生式继承
    • 寄生组合式继承
    • 类的继承

原型链继承

  • 将父类的实例作为子类的原型来实现继承
  • 缺点:多个实例对象共享一个原型对象
// 原型链继承
function Parent() {
  this.name = 'parent';
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child() {
  this.age = 18;
}

// 关键:将父类的实例作为子类的原型
Child.prototype = new Parent();
Child.prototype.constructor = Child; // 修复constructor指向

// 测试
const child1 = new Child();
const child2 = new Child();
console.log(child1.getName()); // parent
child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green', 'black'] - 引用类型被所有实例共享

构造函数继承

  • 在子类构造函数中调用父类构造函数来实现属性继承
  • 缺点:无法继承父类原型上的方法
// 构造函数继承
function Parent() {
  this.name = 'parent';
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child() {
  // 关键:调用父类构造函数
  Parent.call(this);
  this.age = 18;
}

// 测试
const child1 = new Child();
const child2 = new Child();
child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green'] - 引用类型不共享
console.log(child1.getName); // undefined - 无法继承原型方法

组合继承

  • 利用原型继承和构造函数继承的特点,刚好可以解决他们的缺点
  • 使用父类构造函数继承属性
  • 设置子构造函数原型为父构造函数实例,继承原型
  • 缺点:父类构造函数重复执行两次,导致实例对象和原型上都有属性,造成浪费
// 组合继承
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  // 继承属性
  Parent.call(this, name); // 第一次调用父类构造函数
  this.age = age;
}

// 继承方法
Child.prototype = new Parent(); // 第二次调用父类构造函数
Child.prototype.constructor = Child;

// 测试
const child1 = new Child('child1', 18);
const child2 = new Child('child2', 20);
child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green']
console.log(child1.getName()); // child1
console.log(child2.getName()); // child2

原型式继承

  • 利用object.create继承对象
  • 缺点:多个实例对象共享一个原型对象
const parent = {
  name: "parent",
  colors: ["red", "blue", "green"],
  getName: function () {
    return this.name;
  },
};

const  child1 = Object.create(parent)
const  child2 = Object.create(parent)

// 等同于 Object
// const  child1 = Object(parent)
// const  child2 = Object(parent)

console.log(child1.getName(), child1.colors); // parent [ 'red', 'blue', 'green' ]
console.log(child2.getName(), child2.colors); // parent [ 'red', 'blue', 'green' ]

child1.name = "child1";
child1.colors.push("black");

child2.name = "child2";

console.log(child1.getName(), child1.colors); // child1 [ 'red', 'blue', 'green', 'black' ]
console.log(child2.getName(), child2.colors); // child2 [ 'red', 'blue', 'green', 'black' ]

寄生式继承

  • 原型式继承的基础上添加方法,增强对象
// 寄生式继承
function createAnother(original) {
  const clone = Object.create(original); // 创建一个新对象
  clone.sayHi = function() { // 增强这个对象
    console.log('hi');
  };
  return clone; // 返回这个对象
}

const parent = {
  name: 'parent',
  colors: ['red', 'blue', 'green']
};

const child = createAnother(parent);
child.sayHi(); // hi
console.log(child.colors); // ['red', 'blue', 'green']

寄生组合式继承

  • 结合了组合继承和寄生继承的优点
  • 解决了继承的所有问题,是最优解
// 组合继承
function Parent(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

Parent.prototype.getName = function () {
  return this.name;
};

function Child(name, age) {
  // 继承属性
  Parent.call(this, name); // 第一次调用父类构造函数
  this.age = age;
}

// 寄生组合式继承 解决两次调用构造函数的方式
Child.prototype = Object.create(Parent.prototype); // 创建一个新的对象,作为Child的原型,如果后续修改Child的原型不会影响Parent的原型
Child.prototype.constructor = Child;

// 测试
const child1 = new Child("child1", 18);
const child2 = new Child("child2", 20);
child1.colors.push("black");
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green']
console.log(child1.getName()); // child1
console.log(child2.getName()); // child2

类的继承

  • ES6推出了class关键字,可以像一般面向对象语言一样使用class关键字定义对象,但本质上class是一种语法糖,底层还是使用原型实现的实例对象和继承
  • 实现的效果和寄生组合式继承一样,实例属性不会相互影响
class Parent {
  constructor() {
    this.name = "parent";
    this.colors = ["red", "blue", "green"];
  }

  getName() {
    return this.name;
  }
}

class Child extends Parent {
  constructor(name) {
    super(); // 继承时必须调用父类构造函数
    this.name = name;
  }

  // 静态方法,相当于直接在Child构造函数上挂方法 Child.sayHi
  static sayHi() {
    console.log("hi child");
  }
}

const child1 = new Child('child1', 18);
const child2 = new Child('child2', 20);
child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green']
console.log(child1.getName()); // child1
console.log(child2.getName()); // child2
❌
❌