学习——《你不知道的JavaScript》上卷

一、作用域的认识

1.1LHS和RHS

        LHS:赋值操作的目标是谁。比如a=2,那么就是把2赋值给a,就是一个LHS操作。

        RHS:谁是赋值操作的源头。比如console.log(a),就是查找a的值,是一个RHS操作。

1.2.作用域嵌套

        当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用 域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。

1.3.异常处理

        观察如下代码:

function foo(a) {
    console.log( a + b );
    b = a;
}
foo( 2 );

        在第一次RHS b 的时候,并没有找到b的值,当他找遍所有作用域都找不到b的话,就会报ReferenceError 异常。

在非严格模式下,如果LHS没有查询到赋值的目标变量,会帮你创建一个。如果是严格模式,那也会报ReferenceError 异常。

如果RHS查询到了值,但是你对这个值做了不合规的操作,那么会报TypeError。

二、词法作用域

2.1 词法阶段

        观察以下代码

    function foo(a) {
        var b = a * 2;
        function bar(c) {
            console.log( a, b, c );
            }
        bar( b * 3 );
    }
    foo( 2 ); // 2, 4, 12

        上述代码中,有三个作用域,分别是全局作用域,foo函数作用域,bar函数作用域。

        在bar函数在查找abc时,a和b都查找不到,所以要到上层作用域去查找,知道第一次找到匹配的标识符停止,如果外层又定义了匹配的标识,那么将被遮蔽,我们叫它遮蔽效应。

        作用域的查找使用是从最内层开始查找,直到找到第一次匹配的标识停止。

        可以利用window.a来访问全局作用域中的被遮蔽的a的值,但是如果不是在全局作用域中被遮蔽的值无法访问到。

        无论函数在哪里被调用,如何被调用,他的词法作用域始终由他声明的位置决定。

2.2 eval

    function foo(str, a) {
        eval( str ); // 欺骗!
        console.log( a, b );
    }
    var b = 2;
    foo( "var b = 3;", 1 ); // 1, 3

        eval将var b = 3放到了foo函数中,这样修改了foo的词法作用域,在查找b时会查找到3,而全局作用域中的2被遮蔽了。如果是严格模式,则会报ReferenceError 异常。

2.3 with

                快捷的访问obj里面的abc的值。

with (obj) {
    a = 3;
    b = 4;
    c = 5;
}

        观察以下代码:

function foo(obj) {
    with (obj) {
        a = 2;
    }
}
var o1 = {
    a: 3
};
var o2 = {
    b: 3
};

foo( o1 );
console.log( o1.a ); // 2

foo( o2 );
console.log( o2.a ); // undefined

console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!

        当我们把o1传入时,with的作用域实际上是o1,那么进行LHS查询,会改变o1中的a的值。当把o2传入时,with的作用域是o2,但是o2没有a,foo内没有a,全局没有a,所以在进行LHS查询时,会自动的帮我们新建一个全局的变量a,将2赋值给a。

三、函数作用域

        函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复 用(事实上在嵌套的作用域中也可以使用)。

3.1隐藏内部实现——最小暴露原则

        这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来。例如如下代码:

    function doSomething(a) {
        b = a + doSomethingElse( a * 2 );
        console.log( b * 3 );
    }
    function doSomethingElse(a) {
        return a - 1;
    }
    var b;
    doSomething( 2 ); // 15

        上述代码在doSomething中应该包含着b和doSomethingElse(),但是却在全局中声明的这两个变量,导致外部可以访问到,可能会造成奇奇怪怪的错误。所以我们尽量将b和doSomethingElse封装在doSomething()中。

    function doSomething(a) {
        function doSomethingElse(a) {
            return a - 1;
        }
        var b;
        b = a + doSomethingElse( a * 2 );
        console.log( b * 3 );
    }
    doSomething( 2 ); // 15

         现在我们虽然将他们封装在了doSomething()中,但其实我们在全局作用域中添加了一个doSomething的具名函数,就已经在污染作用域了,并且我们必须要调用该函数才会执行内部的代码。

        所以JavaScript给我们提供了一个解决方案:

    (function doSomething(a) {
        function doSomethingElse(a) {
            return a - 1;
        }
        var b;
        b = a + doSomethingElse( a * 2 );
        console.log( b * 3 );
    })()
    

         上述为立即执行函数(IIFE),doSomething函数被封装在了()中,所以外部的作用域不会访问到他。

3.1.1匿名函数表达式和具名函数表达式

        最常用的匿名函数表达式可能就是回调函数了,比如

    setTimeout( function() {
        console.log("I waited 1 second!");
    }, 1000 );

        其中的function即为一个匿名函数表达式,不过匿名有匿名的缺点:

  • 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  • 如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用, 比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑 自身 。
  • 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

        所以我们还是需要函数名称的,所以我们可以用行内函数表达式:

    setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
        console.log( "I waited 1 second!" );
    }, 1000 );

3.1.2立即执行函数表达式

        上述我们已经提到了IIFE,由于函数被包在了()内部,那么他就是一个表达式,用后面的()来调用。IIFE使用匿名函数和具名函数都可以。他的另一个进阶的玩法就是传参:

    var a = 2;
    (function IIFE( global ) {
        var a = 3;
        console.log( a ); // 3
        console.log( global.a ); // 2
    })( window );
    console.log( a ); // 2

        将window传入函数,这样就可以访问到外部全局作用于中a的值。 

        当然也可以把一个函数传到这个IIFE中。

    (function IIFE( def ) {
        def( window );
    })(function def( global ) {
    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2
    });

3.2如何规避同名冲突

        两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致 变量的值被意外覆盖。

    function foo() {
        function bar(a) {
            i = 3; // 修改 for 循环所属作用域中的 i
            console.log( a + i );
        }
        for (var i=0; i<10; i++) {
            bar( i * 2 ); // 糟糕,无限循环了!
        }
    }
    foo();

        上述代码中循环的i被上面bar()中的i覆盖为了3,一直<10,所以无限循环。这就是同名冲突,我们可以将bar()中的i=3更改为var i = 3,这样,根据遮蔽原理,优先找到了循环的i,就可以正常运行,或者用一个其他变量j其实更好。

3.3块作用域

3.3.1 try catch

        try/catch 的 catch 分句会创建一个块作 用域,其中声明的变量仅在 catch 内部有效 。

3.3.2 let

        下面的代码,利用let声明bar,就将bar锁在了if的块作用域中,外部是无法访问到bar的。 并且let声明也不会有变量提升。

    var foo = true;
    if (foo) {
        let bar = foo * 2;
        bar = something( bar );
        console.log( bar );
    }
    console.log( bar ); // ReferenceError

3.3.3 const

        在es6中除了let还引入了const,他定义的是一些常量,任何试图修改值的操作 都会报错。

    var foo = true;
    if (foo) {
        var a = 2;
        const b = 3; // 包含在 if 中的块作用域常量
        a = 3; // 正常 !
        b = 4; // 错误 !
    }
    console.log( a ); // 3
    console.log( b ); // ReferenceError!

四、 提升

4.1先声明,再赋值(调用)

        下面的代码中,有两个提升:foo的函数声明提升,和var a的变量声明提升。

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

        需要注意,函数会优先提升。下面的代码会打印出1二不是2,说明foo的函数提升优先于变量提升。

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

五、作用域闭包

5.1闭包的实质:

        当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。我们来看如下代码:

    function foo() {
        var a = 2;
        function bar() {
            console.log( a );
        }
        return bar;
    }
    var baz = foo();
    baz(); // 2 —— 朋友,这就是闭包的效果。

        上述代码中,bar的词法作用域可以访问到外服的foo的函数作用域,将bar()作为foo的返回值,当foo调用时,将返回的bar给到了baz,调用baz时其实就是调用了内部的bar,这样,我们就从外部执行了bar()。

        通常foo()在执行后会进行垃圾回收机制,将内部作用域全部销毁,但是闭包的神奇之处在于,bar在使用foo的内部作用域,所以foo()的作用域就一直存活下去供bar()使用。

        bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

        当foo被调用后的几微妙后,baz被调用,foo内部的bar就被调用了,不出所料的可以打印出a的值。

        再比如我们观察如下代码:

    function foo() {
        var a = 2;
        function baz() {
            console.log( a ); // 2
        }
        bar( baz );
    }
    function bar(fn) {
        fn(); // 妈妈快看呀,这就是闭包!
    }

        上述代码中调用的是bar的参数fn函数,也就是baz,所以也是在函数外部调用了baz,这也是闭包。

5.2闭包与循环

        我们观察如下代码,我们想让他输出12345,但是他却输出了5个6,这是为什么呢?

    for (var i=1; i<=5; i++) {
        setTimeout( function timer() {
            console.log( i );
        }, i*1000 );
    }

        因为延时函数的回调函数是在循环后进行的,所以每一个回调都调用的同一个i,输出6个6。我们希望在每次迭代运行后都能捕获到新的i,所以我们在每次循环迭代中都需要闭包作用域。

        利用之前学过可以创造作用域的方法,IIFE是个不错的选择:

    for (var i=1; i<=5; i++) {
        (function(j) {
            setTimeout( function timer() {
                console.log( j );
            }, j*1000 );
        })( i );
    }

        这样timer中的j访问的就是IIFE作用域中每次迭代传递进来的新的i了。或者选用let建立块作用域也可以。

    for (var i=1; i<=5; i++) {
        let j = i; // 是的,闭包的块作用域!
        setTimeout( function timer() {
            console.log( j );
        }, j*1000 );
    }

        timer中的j会访问到了块级作用域中的j,这个j是每次迭代后的i的值,所以成功。如果在for循环的头部使用let来定义,还有一个特殊功能,即每个迭代都会使用上次迭代的值来初始化这个变量,就相当于替代了let j = i。

    for (let i=1; i<=5; i++) {
        setTimeout( function timer() {
            console.log( i );
        }, i*1000 );
    }

5.3 模块

        观察如下代码:这个模式就是模块。CoolModule是一个函数,我们在调用该函数的时候创建了一个模块实例,这个函数的返回值包括了对内部函数的使用,那么我们把这个返回值给到变量foo,就可以通过foo.doSomething来使用方法。

    function CoolModule() {
        var something = "cool";
        var another = [1, 2, 3];
        function doSomething() {
            console.log( something );
        }
        function doAnother() {
            console.log( another.join( " ! " ) );
        }
        return {
            doSomething: doSomething,
            doAnother: doAnother
        };
    }

    var foo = CoolModule();
    foo.doSomething(); // cool
    foo.doAnother(); // 1 ! 2 ! 3

        模块也可以使用IIFE来定义,并且可以传递参数,也可以改变模块实例

    var foo = (function CoolModule(id) {
        function change() {
            // 修改公共 API
            publicAPI.identify = identify2;
        }
        function identify1() {
            console.log( id );
        }
        function identify2() {
            console.log( id.toUpperCase() );
        }
        var publicAPI = {
            change: change,
            identify: identify1
        };
        return publicAPI;
    })( "foo module" );
    foo.identify(); // foo module
    foo.change();
    foo.identify(); // FOO MODULE

六、this的使用

6.1绑定规则——取决于调用位置

6.1.1默认绑定

        这是一种最常见的调用类型,独立函数调用。foo()调用在全局作用域中,所以this就是全局作用域。

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

6.1.2隐式绑定

        直接看代码:foo是在obj对象中调用的,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。如果说是链式调用,只和最后一层的位置有关。

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

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

        隐式丢失:他虽然调用的还是obj中的foo,但是因为bar()是一个独立调用的函数,这时候的this会绑定在全局对象上。

    function foo() {
        console.log( this.a );
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var bar = obj.foo; // 函数别名!
    var a = "oops, global"; // a 是全局对象的属性
    bar(); // "oops, global"

        当我们在执行传入的回调函数时,也是类似。调用的是obj中的foo但是是再fn()处执行的。所以this指向doFoo函数对象,doFoo是再全局中调用,所以this指向全局。

    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"

        在定时器中传入obj中的foo执行也是一样的,因为定时器的执行规律如下伪代码所示

    function setTimeout(fn,delay) {
        // 等待 delay 毫秒
        fn(); // <-- 调用位置!
    }

6.1.3显示绑定——call(),apply()

        1.硬绑定:显示的强制绑定,我们称之为硬绑定。下面是硬绑定的经典应用场景:创建一个包裹函数,传入所有的参数并返回接收到的所有值:

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

        在 ES5 中提供了内置的方法 Function.prototype. bind,它的用法如下:

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

6.1.4 new绑定

        首先我们明确一个概念,所有的函数都可以被new调用,这种被调用的函数叫构造函数。实际上并不存在所谓的“构造函数”,存在的是对函数的“构造调用”。

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

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

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

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

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

        在使用new的时候,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。

6.2 优先级

        知道了4种this的绑定形式,那我们来讨论一下他们的优先级。显然默认绑定的优先级最低,那么我们先来看一下显示绑定和隐式绑定,他们的优先级谁高一点?

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

        显然,显示绑定的优先级要高一点,所以我们可以优先使用显示绑定。

        现在我们来搞清楚 new 绑定和显示绑定的优先级谁高谁低??因为new 和 call/apply 无法一起使用,所以我们可以通过硬绑定的方法来测试他们的优先级。

    function foo(something) {
        this.a = something;
    }
    var obj1 = {};

    var bar = foo.bind( obj1 );
    bar( 2 );
    console.log( obj1.a ); // 2

    var baz = new bar(3);
    console.log( obj1.a ); // 2
    console.log( baz.a ); // 3

        通过上面的代码我们可以发现,new bar(3)并没有改变obj1中的a的值,二是将3给到了baz这个新对象中,所以new的优先级要大于显示绑定的优先级。

        所以!new>显示>隐式>默认绑定,这样this的绑定就会判断了把!但是也有例外的

6.3绑定例外

6.3.1被忽略的this

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

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

        那么我们为什么要传入null呢,比如用apply展开数组,比如用bind预置参数。

    function foo(a,b) {
        console.log( "a:" + a + ", b:" + b );
    }
    // 把数组“展开”成参数,在ES6中可以用...来实现
    foo.apply( null, [2, 3] ); // a:2, b:3
    // 使用 bind(..) 进行柯里化
    var bar = foo.bind( null, 2 );
    bar( 3 ); // a:2, b:3

        但是总是使用null和undefined来忽略this会产生一个副作用,因为这样将this忽略成全局对象,可能会修改全局对象的某些属性,造成奇奇怪怪的bug。所以我们有一种更安全的this——交给Object.create(null),他是一个没有protoype的{}.

    function foo(a,b) {
        console.log( "a:" + a + ", b:" + b );
    }
    // 我们的 DMZ 空对象
    var ø = Object.create( null );
    // 把数组展开成参数
    foo.apply( ø, [2, 3] ); // a:2, b:3
    // 使用 bind(..) 进行柯里化
    var bar = foo.bind( ø, 2 );
    bar( 3 ); // a:2, b:3

6.3.2间接引用

        间接引用一般发生在赋值时,将o中的foo赋值给了p,所以实际调用的是foo函数。他的this就是全局。

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

第七章、对象

7.1 语法

        对象可以通过两种形式来定义,声明形式、构造形式。

        声明形式即 var obj = {a:123},构造形式即 var obj = new Object(); obj.a = b。他们的唯一区别就是构造形式只能一个一个添加键值对。

7.2 内置对象

        Js中有一些对象的子类型,我们叫做内置对象。他们实际上就是一些内置函数,当然可以用new调用成构造函数来使用。这样就会构造一个确定子类型的对象。

var strObject = new String( "I am a string" );

        当我们使用声明形式来定义对象时,其实他是一个字面量,并不是一个String对象,如果我们想获取到它的长度等一些操作时,就必须把他转换为String对象,好麻烦。幸好js在必要时会自己转换,所以使用声明式比较多。

var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"

        比如上述两种方法就会自动进行转换,数字和布尔类型也是如此,null和undefined只有声明形式,Date只有构造形式。

7.3 内容

        我们在访问对象中的内容可以用以下两种方法:

        obj.a和obj[a],如果a是不符合要求的字符串,则只能用obj[a]。

        这里的a无论是用123还是true还是其他,它永远都会转换为字符串的形式。

7.3.1可计算属性名

        ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名:

    var prefix = "foo";
    var myObject = {
        [prefix + "bar"]:"hello",
        [prefix + "baz"]: "world"
    };
    myObject["foobar"]; // hello
    myObject["foobaz"]; // world

7.3.2属性与方法

        当对象的属性是一个函数时,一般喜欢叫他方法,但是如下代码除了对应的this可能有所不同以外,都是在调用foo,所以函数和方法在js中可以互换

    function foo() {
        console.log( "foo" );
    }
    var someFoo = foo; // 对 foo 的变量引用
    var myObject = {
        someFoo: foo
    };
    foo; // function foo(){..}
    someFoo; // function foo(){..}
    myObject.someFoo; // function foo(){..}

7.3.3数组

        数组也可以通过[]来访问内容,比如arr[1],但是这里期望[]中为数字,不过我们还是可以给数组添加属性,比如arr.a = b,这样arr中就多了一个属性,不过他的length是不变的。如果添加的属性名看起来是一个数字,那么就会改变数组的长度。

7.3.4复制对象

        对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解 析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:

var newObj = JSON.parse( JSON.stringify( someObj ) );

        对于浅拷贝来说,ES6 定义了 Object.assign(..) 方 法来实现浅复制。第一个参数为目标对象,第二个为拷贝对象。

var newObj = Object.assign( {}, myObject );

7.3.5属性描述符

        从ES5之后,所有的属性都具有了属性描述符。

    var myObject = {};
    Object.defineProperty( myObject, "a", {
        value: 2,
        writable: true,
        configurable: true,
        enumerable: true
    } );
    myObject.a; // 2

        其中writable为是否可以修改;configurable为是否可配置,如果设置为false则不能配置回true;enumerable为是否可枚举。(是否可循环遍历)

7.3.6不变性

        1.常量:如果我想创建一个不可修改的常量属性,可以设计writable:false 和 configurable:false。

        2.禁止拓展:如果想禁止一个对象添加新属性,并且保留已有属性,以 使 用 Object.prevent Extensions(..):

        3. 密封 Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。 所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)。

        4. 冻结 Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们 的值。

7.3.7存在性

        我们可以在不访问属性值的情况下判断是否存在该属性。

    var myObject = {a:2};
    ("a" in myObject); // true
    myObject.hasOwnProperty( "a" ); // true

        in会检查到原型链,hasOwnProperty不检查原型链。

7.4遍历

        for in 可以遍历对象的可枚举属性列表

        for of 可以遍历到数组中的所有属性值。它会通过调用迭代器对象的next()来遍历所有值。

    var myArray = [ 1, 2, 3 ];
    var it = myArray[Symbol.iterator]();
    it.next(); // { value:1, done:false } 
    it.next(); // { value:2, done:false } 
    it.next(); // { value:3, done:false } 
    it.next(); // { done:true }

        因为数组中有iterator,所以可以直接使用,但对象中没有,如果我们想要使用for of需要手动写。

var myObject = {
    a: 2,
    b: 3
};
Object.defineProperty(myObject, Symbol.iterator, {
    enumerable: false,
    writable: false,
    configurable: true,
    value: function () {
        var o = this;
        var idx = 0;
        var ks = Object.keys(o);
        return {
            next: function () {
                return {
                    value: o[ks[idx++]],
                    done: (idx > ks.length)
                };
            }
        };
    }
});

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值