彻底搞懂JavaScript的this指向问题

这篇博客详细探讨了JavaScript中this关键字的使用和其指向问题。文章指出,this不是指向函数自身,而是根据函数调用方式动态绑定。作者讲解了this的四种绑定规则:默认绑定、隐式绑定、显式绑定和new绑定,并阐述了它们的优先级。此外,还提到了箭头函数的特殊情况,箭头函数不绑定this,而是继承外层作用域的this。博客最后给出了相关面试题以帮助读者巩固理解。

this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的 JavaScript 开发者也很难说清它到底指向什么。

一、为什么要用this?

下面我们来解释一下为什么要使用 this:

    const me = { name: "xiaoming" };
    const you = { name: "xiaohua" };
		function identify() {
      return this.name.toUpperCase();
    }
    function speak() {
      var greeting = "Hello, I'm " + identify.call(this);
      console.log(greeting);
    }
    identify.call(me); // XIAOMING
    identify.call(you); // XIAOHUA 
    speak.call(me); // Hello, 我是 XIAOMING
    speak.call(you); // Hello, 我是 XIAOHUA

这段代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(), 不用针对每个对象编写不同版本的函数。

如果不使用 this,那就需要给 identify() 和 speak() 显式传入一个上下文对象。

    const me = { name: "xiaoming" };
    const you = { name: "xiaohua" };
    function identify(context) {
      return context.name.toUpperCase();
    }
    function speak(context) {
      var greeting = "Hello, I'm " + identify(context);
      console.log(greeting);
    }
    identify(you); // XIAOHUA 
    speak(me); //hello, 我是 XIAOMING

this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计 得更加简洁并且易于复用。

随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。

二、误解
指向自身

第一种误解就是this指向函数自身。分析一下下面的代码:

  function foo(num) {
    console.log("foo: " + num); // 记录 foo 被调用的次数
    // foo: 6
    // foo: 7
    // foo: 8
    // foo: 9
    console.log(this); // Window
    this.count++;
    console.log(this.count); // -- 第一次为undefined,后续为NaN
    // undefined
    // NaN
    // NaN
    // NaN
  }
  foo.count = 0;
  for (let i = 0; i < 10; i++) {
    if (i > 5) {
      foo(i);
    }
  }
  // foo 被调用了多少次?
  console.log(foo.count); // 0
  console.log(window.count); // NaN

如果说this指向自身(foo)的话,console.log(foo.count);得到的结果应该是4,结果却是0。

开发者一定会问“如果我增加的 count 属性和预期的不一样,那我增加的是哪个 count ?”实际上,如果他深入探索的话,就会发现这段代码在无意中创建了一个全局变量 count(后续讲解),它的值为 NaN。当然, 如果他发现了这个奇怪的结果,那一定会接着问:“为什么它是全局的,为什么它的值是 NaN 而不是其他更合适的值?”(后续讲解)

三、this到底是什么?

排除了错误理解之后,我们来看看 this 到底是一种什么样的机制。

this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

当一个函数被调用时,会创建一个活动记录(也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。

所以说,我们要学习如何寻找函数的调用位置,从而判断函数在执行过程中会如何绑定this。

四、调用位置

**调用位置:**调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

**调用栈:**为了到达当前执行位置所调用的所有函数。

下面我们来看看到底什么是调用栈和调用位置:

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

注意:我们是如何(从调用栈中)分析出真正的调用位置的,因为它决定了 this 的绑定。

五、绑定规则

我们来看看在函数的执行过程中调用位置如何决定 this 的绑定对象。

你必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。我们首先会分别解释这四条规则,然后解释多条规则都可用时它们的优先级如何排列。

1、默认绑定

首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

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

  function foo() {
    "use strict"; 
    console.log(this); // undefined
    console.log(this.a); // Uncaught TypeError: Cannot read property 'a' of undefined
  }
  let a = 2;
  foo();

**(2)**如果非严格模式(non-strict mode)下时,默认绑定才能绑定到全局对象Window:

  function foo() {
    console.log(this); // Window
    console.log(this.a); // 2
  }
  let a = 2;
  foo();
2、隐式绑定

隐式绑定需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。

分析一下下面的代码:

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

首先需要注意的是 foo() 的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。但是无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj 对象。

然而,调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥有”或者“包含”它。

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

举例说明:

  function foo() {
    console.log(this.a);
  }
  var obj2 = { a: 42, foo: foo };
  var obj1 = { a: 2, obj2: obj2 };
  obj1.obj2.foo(); // 42

⭐️隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。

举例说明:

  function foo() {
    console.log(this.a);
  }
  var obj = { a: "这是obj对象的a属性", foo: foo };
  var bar = obj.foo; // 指向foo函数自身的指针赋值给了bar
  var a = "这是全局对象的a属性"; // a 是全局对象的属性
  bar(); // "这是全局对象的a属性"

这里的obj的foo属性存放的其实是foo函数的指针,而var bar = obj.foo;这个语句其实就是把这个函数的指针赋值给了bar,那么调用bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

一种更微妙、更常见的情况发生在传入回调函数时:

  function foo() {
    console.log(this.a);
  }
  function doFoo(fn) {
    // fn 其实引用的是 foo
    fn(); // <-- 调用位置!
  }
  var obj = { a: 2, foo: foo };
  var a = "这是全局对象的a属性"; // a 是全局对象的属性
  doFoo(obj.foo); // "这是全局对象的a属性"

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

3、显式绑定

隐式绑定的前提:我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

**1、call() 、apply()**方法

JavaScript 中的“所有”函数都有一些有用的特性(这和它们的 [[ Prototype ]] 有关),可以解决这个问题。

  • 参数:call() 和 apply()的第一个参数都是对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this;
  • 区别:后面的参数,apply()为数组,call()为参数列表;

思考下面代码:

  function foo() {
    console.log(this);
  }
  let obj = { a: 2 };
  let num = 2;
  // call()
  foo.call(obj); // {a: 2}
  foo.call(num); // Number {2}
  foo.call(window); // window
  // apply()
  foo.apply(obj); // {a: 2}
  foo.apply(num); // Number {2}
  foo.apply(window); // window

通过 foo.call()或者foo.apply(),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上,如果传入的第一个参数是字符串类型、布尔类型或者数字类型,来当作this的绑定对象,这个参数会被转换成他的对象形式(也就是 new String()、new Boolean() 或者new Number()),这通常被称为“装箱”。

2、bind函数

如果我们希望一个函数总是显示的绑定到一个对象上,可以怎么做呢?

方案一:创建一个辅助函数:

  function foo(something) {
    console.log(this.a, something); // 2 3
    return this.a + something;
  }
  // 简单的辅助绑定函数
  function bind(fn, obj) {
    return function () {
      return fn.apply(obj, arguments);
    };
  }
  var obj = { a: 2 };
  var bar = bind(foo, obj);
  var b = bar(3);
  console.log(b); // 5

方案二:Function.prototype.bind

  function foo(something) {
    console.log(this.a, something); // 2 3
    return this.a + something;
  }
  var obj = { a: 2 };
  var bar = foo.bind(obj);
  var b = bar(3);
  console.log(b); // 5

3、内置函数

案例一:setTimeout

setTimeout(function() {
  console.log(this); // window
}, 1000);

为什么是window呢?

这个和setTimeout源码内部调用有关,setTimeout内部是通过apply进行绑定的this对象,并且绑定的是全局对象。

案例二:forEach

数组有一个用于遍历的高阶函数forEach

  [1,2,3].forEach(function (item) {
    console.log(this); // 三次window
  });

在forEach中传入的函数打印的也是Window对象,这是因为默认情况下传入的函数是自动调用函数(默认绑定)。

那么我们是否可以改变this的指向呢?怎么改变呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LrXc9jaP-1624876111702)(/Users/liumingjun/Library/Application Support/typora-user-images/image-20210628142605860.png)]

  var obj = { a: 2 };
  [1, 2, 3].forEach(function (item) {
    console.log(this); // 三次obj对象
  }, obj);

案例三:DOM点击事件

假设有一个div,并给当前的div加上点击事件:

<style>
  .box {
    width: 200px;
    height: 200px;
    background-color: red;
  }
</style>

<div class="box"></div>
  var box = document.querySelector(".box");
  box.onclick = function () {
    console.log(this); // box对象
  };

在点击事件的回调中,this指向谁呢?box对象。这是因为在发生点击时,执行传入的回调函数被调用时,会将box对象绑定到该函数中;

4、new绑定

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

  1. 创建(或者说构造)一个全新的对象。

  2. 这个新对象会被执行 [[ Prototype ]] 连接。

  3. 这个新对象会绑定到函数调用的 this。

  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

// 创建Person
function Person(name) {
  console.log(this); // Person {}
  this.name = name;
}
var person = new Person("xiaoming");
console.log(person); // Person {name: "xiaoming"}
六、优先级

现在我们已经了解了函数调用中 this 绑定的四条规则,你需要做的就是找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则,这些绑定规则的优先级谁更高呢?

1、默认绑定规则优先级最低

毫无疑问,默认绑定的优先级是四条规则中最低的,因为应用其他的规则就会明确绑定this。

2、显式绑定高于隐式绑定

测试一下:

  function foo() {
    console.log(this.a);
  }
  var obj1 = { a: 1, foo: foo };
  var obj2 = { a: 2, foo: foo };
  obj1.foo(); // 1 隐式绑定obj1
  obj2.foo(); // 2 隐式绑定obj2
  obj1.foo.call(obj2); // 2 隐式绑定obj1 + 显式绑定obj2
  obj2.foo.call(obj1); // 1 隐式绑定obj2 + 显式绑定obj1

由此可以看出:显式绑定高于隐式绑定。

3、new绑定高于隐式绑定

测试一下:

  function foo() {
    console.log(this);
  }
  var obj = {
    name: "xiaoming",
    foo: foo,
  };
  new obj.foo(); // foo对象, 说明new绑定优先级更高

由此可以看出:new绑定高于隐式绑定。

4、new绑定高于bind绑定

new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高。

测试一下:

  function foo() {
    console.log(this);
  }
  var obj = {
    name: "xiaoming",
    foo: foo,
  };
  new foo.call(obj);  //Uncaught TypeError: foo.call is not a constructor

new绑定能不能和bind同时使用呢?可以的。

测试一下:

  function foo() {
    console.log(this);
  }
  var obj = {
    name: "xiaoming",
    foo: foo,
  };
  var bar = foo.bind(obj); 
  bar(); // obj对象 {name: "xiaoming", foo: ƒ}
  new bar(); // foo {}

由此可以看出:new绑定高于bind绑定。

优先级总结:

new绑定 > bind绑定 > 隐式绑定 > 默认绑定

七、判断this

介绍完绑定规则的优先级,我们可以通过下面顺序进行判断:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。var bar = new foo()

  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。var bar = foo.call(obj2)

  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。var bar = obj1.foo()

  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。var bar = foo()

八、规则之外

规则总有例外,这里也是一样。在某些场景下 this 的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。

1、忽略显式绑定

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

分析代码:

  function foo() {
    console.log(this);
  }
  var obj = {name: "xiaoming"};
  foo.call(obj); // obj对象 {name: "xiaoming"}
  foo.call(null); // window
  foo.call(undefined); // window
  foo.bind(null)(); // window
2、间接引用

我们先来看下面的案例结果是什么?

分析代码:

  var num1 = 1;
  var num2 = 0;
  var result = (num2 = num1);
  console.log(result); // 1 -> num1

(num2 = num1)的结果是num1的值;

分析代码:

  function foo() {
    console.log(this.a);
  }
  var a = 2;
  var o = { a: 3, foo: foo };
  var p = { a: 4 };
  o.foo(); // 3 o对象
  (p.foo = o.foo)(); // 2 window

赋值表达式 p.foo = o.foo 的返回值是foo函数的引用,因此调用位置是 foo() 而不是p.foo() 或者 o.foo(),这里会应用默认绑定。

注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则this 会被绑定到全局对象。

3、ES6箭头函数

箭头函数不使用 this 的四种标准规则,而是根据外层作用域来决定 this。

  function foo() {
    // 返回一个箭头函数
    return (a) => {
      //this 继承自 foo()
      console.log(this.a);
    };
  }
  var obj1 = { a: 2 };
  var obj2 = { a: 3 };
  var bar = foo.call(obj1);
  bar.call(obj2); // 2, 不是 3 !

foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1,bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)

分析代码:

  function foo() {
    setTimeout(function () {
      console.log(this.a); // 1 如果想输出obj的a,怎么做呢?
    }, 100);
  }
  var obj = { a: 2,foo: foo};
  var a = 1;
  obj.foo();

ES6之前是我们最常用的方式:

  function foo() {
    var _this = this; // 将当前的this赋值给变量_this
    setTimeout(function () {
      console.log(_this.a); // 2 通过变量_this获取obj对象
    }, 100);
  }
  var obj = { a: 2,foo: foo};
  var a = 1;
  obj.foo();

ES6箭头函数处理方式:

  function foo() {
    setTimeout(()=>{
      console.log(this.a); // 2
    }, 100);
  }
  var obj = { a: 2,foo: foo};
  var a = 1;
  obj.foo();

由此可以看出:箭头函数不绑定this,会继承外层函数调用的 this 绑定。

九、复习与巩固

精选面试题一:

var name = "window";
var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};
function sayName() {
  var newSayName = person.sayName;
  newSayName();
  person.sayName();
  (person.sayName)();
  (b = person.sayName)();
}
sayName();

精选面试题二:

var name = 'window';
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

person1.foo1();
person1.foo1.call(person2);

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);

精选面试题三:

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1()
person1.foo1.call(person2)

person1.foo2()
person1.foo2.call(person2)

person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)

person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)

精选面试题四:

var name = 'window'
function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)

person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)

精选面试题一答案:

var name = "window";
var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};
function sayName() {
  var newSayName = person.sayName;
  newSayName(); // "window"
  person.sayName(); // "person"
  (person.sayName)(); // "person"
  (b = person.sayName)(); // "window"
}
sayName();

精选面试题二答案:

var name = 'window';
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person2 = { name: 'person2' }

// 隐式绑定
person1.foo1(); // 'person1'

// 显式绑定person2 + 隐式绑定person1,显式绑定生效
person1.foo1.call(person2); // 'person2' 

// foo2()是一个箭头函数,不适用所有的规则,只看上层作用域
person1.foo2(); // 'window' 

// foo2依然是箭头函数,不适用于显式绑定的规则,只看上层作用域
person1.foo2.call(person2); // 'window' 

// foo3()的返回值是一个函数声明,独立函数调用,使用默认绑定
person1.foo3()(); // 'window'

// foo3显式绑定到person2中,但是拿到的返回函数依然是在全局下调用,所以依然是window
person1.foo3.call(person2)(); // 'window'

// 拿到foo3返回的函数,通过显式绑定到person2中,所以是person2
person1.foo3().call(person2); // 'person2'

// foo4()的函数返回的是一个箭头函数,箭头函数的执行找上层作用域,是person1
person1.foo4()(); // 'person1'

// foo4()显式绑定到person2中,并且返回一个箭头函数,箭头函数找上层作用域,是person2
person1.foo4.call(person2)(); // 'person2'

// foo4返回的是箭头函数,箭头函数只看上层作用域
person1.foo4().call(person2); // 'person1'

精选面试题三答案:

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

// 隐式绑定
person1.foo1() // person1

// 隐式绑定person1 + 显式绑定person2,显式绑定生效person2
person1.foo1.call(person2) // person2

// 箭头函数,指向上层作用域person1
person1.foo2() // person1

// 箭头函数,指向上层作用域person1
person1.foo2.call(person2) // person1

// foo3的返回值是一个普通函数声明,在全局作用域下调用,默认绑定window
person1.foo3()() // 'window'

// foo3显式绑定person2,并返回一个普通函数声明,在全局作用域下调用,默认绑定window
person1.foo3.call(person2)() // 'window'

// foo3的返回值是一个普通函数声明,并显式绑定person2
person1.foo3().call(person2) // person2

// foo4的返回值是一个箭头函数声明,指向上层作用域person1
person1.foo4()() // person1

// foo4显式绑定person2,并返回一个箭头函数声明,指向上层作用域person2
person1.foo4.call(person2)() // 'person2'

// foo4的返回值是一个箭头函数声明,和call调用没有关系,找到上层的person1
person1.foo4().call(person2) // person1

精选面试题四答案:

var name = 'window'
function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

// foo1返回值是一个普通函数声明,在全局作用域中调用,默认绑定,指向window
person1.obj.foo1()() // 'window'

// foo1显式绑定person2,返回值是一个普通函数声明,在全局作用域中调用,默认绑定,指向window
person1.obj.foo1.call(person2)() // 'window'

// foo1返回值是一个普通函数声明,该函数显式绑定person2,指向person2
person1.obj.foo1().call(person2) // person2

// foo2返回值是一个箭头函数声明,指向上层作用域obj
person1.obj.foo2()() // obj

// foo2显式绑定person2,返回值是一个箭头函数声明,指向上层作用域person2
person1.obj.foo2.call(person2)() // person2

// foo2返回值是一个箭头函数声明,和call调用没有关系,指向上层作用域obj
person1.obj.foo2().call(person2) // obj
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值