js常见面试题

js事件循环机制(event loop)

同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
当指定的事情完成时,Event Table会将回调函数注册到Event Queue。
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。

例如:

console.log(1);
setTimeout(()=>{
	console.log(3);
},100)
console.log(2);

js自上而下执行,先执行console.log(1);输出1,然后执行定时器setTimeout,由于setTimeout是异步的,所以先将setTimeout存放在Event Table,再然后执行console.log(2);输出2,当代码执行完成后会去检测Event Queue,如果有则执行,没有即等待,当100ms过去后setTimeout将进入Event Queue,再然后主线程没有任务执行时将setTimeout取出推入主线程开始执行setTimeout

在这里插入图片描述

微任务与宏任务

宏任务(macrotask )和微任务(microtask ) 表示异步任务的两种分类。常见宏任务:I/O 、setTimeout、setInterval、setImmediate、requestAnimationFrame;微任务:Promise.then catch finally、process.nextTick

例如

setTimeout(()=>{
	console.log(4)
})
new Promise((res,rej)=>{
	console.log(1)
	res()
})
.then(()=>{
	console.log(3)
})
console.log(2)

代码执行先遇到setTimeout,推入Event Table,然后遇到Promise,执行Promise内部代码输出1,再执行res,遇到Promise.then推入Event Table,然后执行输出2,再然后由于Promise.then是微任务,setTimeout是宏任务,所以先将Promise.then推入Event Queue,再推setTimeout,则主线程先执行Promise.then再执行setTimeout;
每次宏任务执行完后都会清空队列里的微任务,微任务执行优先级高于宏任务。

常见的状态码

200,
201表示请求成功且服务器创建了新的资源
202表示服务器已经接受了请求,但还未处理
301,302重定向
304表示自从上一次请求以来,页面的内容没有改变过
404,
500服务器错误
503表示服务器暂时无法处理请求

普通函数可以new吗

可以,但是没有什么意义。(返回一个空的this有何用?)
例如

function a(){

}
console.log(new a());//a{}

addEventListener中第三个参数是干嘛的

控制函数是在捕获阶段执行还是在冒泡阶段执行,默认冒泡阶段false

js垃圾回收机制

当一个变量的生命周期结束之后它所指向的内存就应该被释放。JS有两种变量,全局变量和在函数中产生的局部变量。局部变量的生命周期在函数执行过后就结束了,此时便可将它引用的内存释放(即垃圾回收),但全局变量生命周期会持续到浏览器关闭页面。
回收有两种方式:标记清除、引用计数。
    标记清除:大部分浏览器以此方式进行垃圾回收,当变量进入执行环境(函数中声明变量)的时候,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”,在离开环境之后还有的变量则是需要被删除的变量。
    引用计数:机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时就会被回收。

什么是闭包以及应用场景

函数嵌套函数,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。例如:

function addOne(){
    var a = 10; 
    function fn(){
        a++;
        console.log(a);
    }
    return fn; 
}

应用场景
    如下图,每次切换选项,都会发出请求获取当前选项对应的数据,当反复切换时数据重复请求这样是不是就有点浪费呢?那么我只需要在首次请求时把数据存下来,下次再切换回来时使用存下的数据就可以了吧?
    思路没有问题,但若用户切换过快时,先点击下标1再点击下标2,当下标1数据返回慢时,那数据1就会存储到下标2的位置,此时就会出问题啦,这时候运用闭包就可以完美解决问题啦。

解决闭包造成的内存泄漏
    主动设置为空,打破循环引用

js函数作用域链

js函数作用域链是从定义处开始逐层向外查找,而非被调用处

call、apply和bind的区别

bind不会立即执行,需手动调用; apply, call则是立即调用。
例如

var obj = {
	name : 'wang'
}
function start(){
	console.log(this.name);
}
start.bind(obj)()
start.call(obj)
start.apply(obj)

apply, call的区别

Object.call(this,obj1,obj2,obj3)
Object.apply(this,arguments)

手动实现call、apply和bind

function start(sex,age){
	this.sex = sex;
	this.age = age;
	return this;
}
obj = {
	name : '风舞红枫'
}

mybind

Function.prototype.mybind = function(context) {
	if (typeof this !== 'function') {
		return new Error("不是一个函数");
	}
	console.log(context,arguments)
	var arg = [...arguments].slice(1);
	var that = this;
	return function(){
		return that.apply(context,arg);
	}
}
var result = start.mybind(obj,'男','16')();
console.log(result);

mycall

Function.prototype.mycall = function(context) {
	if (typeof this !== 'function') {
		return new Error("不是一个函数");
	}
	console.log(context,this)
	var arg = [...arguments].slice(1);
	context.fn = this;
	var result = context.fn(...arg);
	delete context.fn;
	return result;
}
var result = start.mycall(obj,'男','16');
console.log(result);

myapply

Function.prototype.myapply = function(context) {
	if (typeof this !== 'function') {
		return new Error("不是一个函数");
	}
	var arg = arguments[1];
	context.fn = this;
	if (arg) {
		result = context.fn(...arguments[1]);
	} else {
		result = context.fn();
	}
	delete context.fn;
	return result;
}
var result = start.myapply(obj,['男','16']);
console.log(result);

this指向

先记以下两条,再往下看
    1、this指向直接调用者;
    2、找不到调用者时即为window;
例1:

function fun(){
   console.log(this.num);
}
var obj = {
    num:'1',
    f:fun
}
var num = 2;
obj.f(); //1 此处是obj调用了函数fun,所以fun函数内this指向obj,故而输出1
fun(); //2	此处无明显调用者,顾指向window,所以输出2

例2(同理):

var A = {
   name: '张三',
   f: function () {
       console.log(this.name);
   }
};
var B = {
   name: '李四'
};
B.f = A.f;
B.f()   //李四
A.f()   //张三

例3:

function foo() {
    console.log(this.a);
}
var obj2 = {
    a: 2,
    fn: foo
};
var obj1 = {
    a: 1,
    o1: obj2
};
obj1.o1.fn(); //2 此处fn的直接调用者是o1,即obj2;

例4:

function C(){
  this.a = 37;
}
var o = new C();
console.log(o.a); //37 构造函数无return时默认返回this
function C2(){
  this.a = 37;
  return {a:38};
}
o = new C2();
console.log(o.a); //38 手动设置了返回对象

例5:

var a = 1;
setTimeout(function(){
	console.log(this.a)	//1 定时器中的this指向window
},100)

例6:

var a = 1;
function(){
	console.log(this.a)	//1 匿名函数的this指向window
}()
让匿名函数中的this指向对象的两种方法
	可以使用对象冒充强制改变this的指向
	将this赋值给一个变量,闭包访问这个变量

call、applay、bind不做例子
箭头函数的this指向为其父级/宿主

原型与原型链

原型
    1、原型就是一个属性,这个属性是构造函数的属性,构造函数是用来制造对象的,是构造函数制造出来的公共祖先,后面所有的对象都会继承原型的属性与方法(原型也是个对象)
    2、__proto__这个是用来查看原型的,这个是对象的属性,这个属性可以查看但是不能修改(隐式属性)
    3、prototype 设置原型,这个是构造函数的属性
原型链
    由于__proto__是任何对象都有的属性,而js里万物皆对象,所以会形成一条__proto__连起来的链条 ,递归访问__proto__必须最终到头,并且值是null。
例如

Object.prototype.toString=function(){
console.log('找到我没有呀?');
}
var func = function(){};
var obj = new func();

console.log(obj.__proto__ === func.prototype) //true
console.log(func.prototype.__proto__ === Object.prototype) //true
// console.log(func.__proto__ === Function.prototype) //true
// console.log(Function.prototype.__proto__ === Object.prototype) //true
console.log(Object.prototype.__proto__ === null)

obj.toString();//此时先在obj中找toString,没有找到后沿着obj.__proto__到了func.prototype。发现func.prototype也没有,再然后沿着func.prototype.__proto__到了Object.prototype,最后找到了toString

console.log(obj.constructor === func) //true
console.log(func.constructor.prototype.__proto__ === Object.prototype) //true
console.log(func.constructor === Function) //true
console.log(func.constructor.__proto__ === Function.prototype) //true
console.log(Function.constructor.prototype.__proto__ === Object.prototype) //true
console.log(Object.constructor.prototype.__proto__ === Object.prototype) //true

继承

原型链继承
即在prototype上new一个你要继承的那个对象(如:Run.prototype=new Show();会覆盖掉原本的prototype)

function Person(){
	this.name="wang";
}
function Boy(){
	this.age="20"; 
}
Boy.prototype=new Person();//Boy继承了Person,通过原型,形成链条
var boy=new Boy();
console.log(boy.name)//wang

构造函数继承(对象冒充继承)
为了解决引用共享和超类型无法传参的问题,我们采用一种叫借用构造函数的技术,或者成为对象冒充(伪造对象、经典继承)的技术来解决这两种问题

function Person(age){
	this.age=age;
}
function Boy(age){
	Person.call(this,age); //对象冒充,给超类型传参
}
var boy = new Boy(22);
console.log(boy.age);//22

组合继承(原型链继承+构造函数继承)
借用构造函数虽然解决了刚才两种问题, 但没有原型, 复用则无从谈起。 所以,我们需要原型链+借用构造函数的模式,这种模式成为组合继承。

function Person(age) {
	this.age = age;
}
Person.prototype.say = function () {
	return '我今年' + this.age + '岁';
};
function Boy(age) {
	Person.call(this, age); //对象冒充
}
Boy.prototype = new Person(); //原型链继承
var boy = new Boy(100);
console.log(boy.say());

原型式继承

function obj(data) { //传递一个字面量函数
	function F() {} //创建一个构造函数
	F.prototype = data; //把字面量函数赋值给构造函数的原型
	return new F(); //最终返回出实例化的构造函数
}
var data = { //字面量对象
	type : '年龄',
	arr : [21,22,23]
};
var data1 = obj(data); //传递
console.log(data1.type); //'年龄'
console.log(data1.arr); //[21,22,23]

data1.type = '年龄呀';
data1.arr.push(24);
console.log(data1.type); //'年龄呀'
console.log(data1.arr); //[21,22,23,24]

var data2 = obj(data); //传递
console.log(data2.type); //'年龄'
console.log(data2.arr); //[21,22,23,24] 引用类型共享了

get与post的区别

其实没有本质的区别,只是习惯get的请求参数放到url中,post的请求参数放到body中。
get请求将参数放在body中也不会报错,post同理,一切只源于潮流…

常用的方法

字符串常用方法
    toLowerCase(): 把字符串转为小写,返回新的字符串。
    toUpperCase(): 把字符串转为大写,返回新的字符串。
    indexOf(): 返回某个指定的子字符串在字符串中第一次出现的位置
    lastIndexOf(): 返回某个指定的子字符串在字符串中最后出现的位置。
    slice(): 返回字符串中提取的子字符串。
    substring(): 提取字符串中介于两个指定下标之间的字符。
    substr(): 返回从指定下标开始指定长度的的子字符串。
    split(): 把字符串分割成字符串数组。
    replace(): 在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
数组常用方法
    push(n): 从数组的尾部添加一个元素n,返回这个添加的元素n
    pop(): 从数组的尾部删除一个元素,返回这个删除的元素,不接收参数
    unshift(n): 从数组的头部添加一个元素n,返回这个添加的元素n,原数组发生改变
    shift(): 从数组的头部删除一个元素,返回这个删除的元素,不接收参数
    slice(): 返回数组中提取的子数组。
    splice(i,n,m,m,m,…): 从数组下标i开始,删除项目n个,然后添加项目m。
    reverse(): 数组翻转,该方法会改变原来的数组,而不会创建新的数组
    sort(): 无参数默认从小到大排序,判断方式:按位判断
    concat(): 数组拼接
    join(‘-’): 将数组转化为为字符串,以括号内的拼接

两=与三=的不同

两=直接判断值是否相等
三=会判断类型是否相同

判断类型的方法有哪些

//typeof
	typeof "";  //string
	typeof 1;   //number
	typeof false; //boolean
	typeof undefined; //undefined
	typeof function(){}; //function
	typeof []; //object
	typeof {}; //object
	typeof Symbol(); //symbol
	typeof null; //object
	typeof new Date(); //object
	typeof new RegExp(); //object
//instanceof
	{} instanceof Object; //true
	[] instanceof Array;  //true
	[] instanceof Object; //true
	"123" instanceof String; //false
	new String(123) instanceof String; //true
//constructor
	"".constructor //ƒ String() { [native code] }
	"".constructor == String //true
	1.constructor //报错 Invalid or unexpected token
	new Number(1).constructor //ƒ Function() { [native code] }
	new Number(1).constructor == Number //false
	new Date().constructor //ƒ Date() { [native code] }
	new RegExp().constructor //ƒ RegExp() { [native code] }
//Object.prototype.toString()
	Object.prototype.toString.call(""); //[object String]
	Object.prototype.toString.call(1); //[object Number]
	Object.prototype.toString.call(false); //[object Boolean]
	Object.prototype.toString.call(undefined); //[object Undefined]
	Object.prototype.toString.call(function(){}); //[object Function]
	Object.prototype.toString.call([]); //[object Array]
	Object.prototype.toString.call({}); //[object Object]
	Object.prototype.toString.call(Symbol()); //[object Symbol]
	Object.prototype.toString.call(null); //[object Null]
	Object.prototype.toString.call(new Date()); //[object Date]
	Object.prototype.toString.call(new RegExp()); //[object RegExp]

封装返回数据类型的方法:

function dataType(obj){
	//return Object.prototype.toString.call(obj).replace(/(\[object )|\]/g,"")
	return Object.prototype.toString.call(obj).replace(/(\[object (\w+)\]/,"$1")
}

null和undefined的区别

null是一个表示"无"的对象,转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。当声明的变量还未被初始化时,变量的默认值为undefined。 null用来表示尚未存在的对象
undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:
    (1)变量被声明了,但没有赋值时,就等于undefined。
    (2)调用函数时,应该提供的参数没有提供,该参数等于undefined。
    (3)对象没有赋值的属性,该属性的值为undefined。
    (4)函数没有返回值时,默认返回undefined。
null表示"没有对象",即该处不应该有值。典型用法是:
    (1)作为函数的参数,表示该函数的参数不是对象。
    (2) 作为对象原型链的终点。

new操作符具体干了什么

1、创建一个空的对象

var obj=new Object();

2、让空对象的原型属性指向原型链,设置原型链

obj.__proto___=Func.prototype;

3、让构造函数的this指向obj,并执行函数体

var result=Func.call(obj);

4、判断返回类型,如果是值就返回这个obj,如果是引用类型,返回这个引用对象。

if (typeof(result) == "object"){
  func=result;
}
else{
    func=obj;
}

ajax原理

1、创建对象
var xhr = new XMLHttpRequest();
2、打开请求
xhr.open(‘GET’, ‘example.txt’, true);
3、发送请求
xhr.send(); 发送请求到服务器
4、接收响应
xhr.onreadystatechange =function(){}
(1)当readystate值从一个值变为另一个值时,都会触发readystatechange事件。
(2)当readystate==4时,表示已经接收到全部响应数据。
(3)当status ==200时,表示服务器成功返回页面和数据。
(4)如果(2)和(3)内容同时满足,则可以通过xhr.responseText,获得服务器返回的内容。

从输入URL到页面加载发生了什么

1、DNS解析
    将域名转化为IP地址的工作。
    如果浏览器有缓存,直接使用浏览器缓存,否则使用本地缓存,再没有就使用host;如果本地没有,就向dns域名服务器查询到对应的IP
    由于dns的解析很耗时,当解析域名过多的时候,便会导致首屏加载变得过慢。
2、TCP连接
    建立连接的三次握手
        客户端:hello,你是服务端么?
        服务端:hello,我是服务端,你是客户端么?
        客户端:yes,我是客户端。
3、发送HTTP请求
4、服务器处理请求并返回HTTP报文
5、浏览器解析渲染页面
    (1)处理HTML标记,构建DOM树;
    (2)处理CSS标记,构建CSSOM树;
        默认情况下,CSS是阻塞加载的,但它并不会阻塞DOM树的解析,它阻塞的是DOM的渲染,直至CSSOM树构建完成。因而为了避免看到长时间的白屏,我们尽可能早地提前加载CSS文件
    (3)将DOM树和CSSOM树合并成渲染树;
    (4)布局渲染树,计算各元素的尺寸、位置;
    (5)绘制渲染树,绘制页面像素信息。
6、连接结束
    断开连接的四次挥手
        主动方:我已经关闭了向你那边的主动通道了
        被动方:收到通道关闭的信息
        被动方:那我也告诉你,我这边向你的主动通道也关闭了
        主动方:最后收到数据,之后双方无法通信

offsetX, clientX, pageX, screenX, layerX, x

offsetX:点击位置距离当前元素左边的距离(padding处算起始点)
clientX:点击位置距离浏览器窗口左边的距离
pageX:点击位置距离文档左边的距离
screenX:点击位置距离屏幕左边的距离
layerX:在点击元素设置相对定位或绝对定位时layerX == offsetX,(layerX是从border处算起始点)
x:xclientX(其他),xlayerX(IE)

防抖和节流

1.防抖(debounce):触发高频事件 n 秒后函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间
    举例:就好像在百度搜索时,每次输入之后都有联想词弹出,这个控制联想词的方法就不可能是输入框内容一改变就触发的,他一定是当你结束输入一段时间之后才会触发。
2.节流(thorttle):高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率。也可以是轮询的稀释,不需要在请求成功后立马再次发出请求。
    举例:就比如抢购时,明明显示的是有产品,购买时却一直提示库存不足,过一会眼睁睁的看着它数值变成了0 。

区别:防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

防抖示例代码

<template>
	<input type="text" @input="change">
</template>

<script>

export default {
    methods: {
		change(){
		   //高频事件函数
		    if(this.sleeptTime){
		        clearTimeout(this.sleeptTime)
		        this.sleeptTime = null;
		    }
		    this.sleeptTime = setTimeout(()=>{
		        this.getList()
		    },1000)
		},
		getList(){
		    console.log('停止输入的1s后进入')
		    //模拟api请求
		    return new Promise((res,rej) => {
		        setTimeout(()=>{
		            res()
		        },500)
		    })
		    .then(res => {
		        console.log('请求成功')
		    })
		}
	}
}
</script>

节流示例代码

getList(){
    //模拟api请求
    new Promise((res,rej) => {
        setTimeout(()=>{
            res()
        },500)
    })
    .then(res => {
        console.log('请求成功')
        //请求成功后间隔1s后重新发起请求
        setTimeout(()=>{
            this.getList();
        },1000)
    })
}

常见的pc端浏览器兼容

1.事件对象创建的兼容:
    var e=e || event
2.鼠标事件对象的属性
    Button属性 左键为0 右键为2 滚轮为1;
    IE 左键为1 右键为2 滚轮为4;
3.键盘事件对对象的属性
    Keycode 获取键盘的按键值 值为AscII码值
    兼容:e.keycode||e.which;
4.阻止事件冒泡兼容
    e.stopPropagation?e.stopPropagation:cancleBubble=true;
5. 事件监听
    绑定事件监听
        addEventListener(); ie:attachEvent()
    取消事件监听
        removeEventListener(); ie:detachEvent()
6. 阻止浏览器的默认行为:
    兼容写法 e.preventDefault?e.preventDefault():e.returnValue=false;
    万能:return false(写在最后面);
7. 事件的委托
    var e.target(其他)||e.srcElement(IE);
8. 获取页面滚走的距离
    document.documentElement.scrollTop||document.body.ScrollTop;
9. 拖拽里面获取鼠标距离按下元素的内部偏移量:
    var disx= e.offsetX||e.layerX;
10.获取某元素的非行内样式:
    window.getComputedStyle?window.getComputedStyle(对象,false)[“属性”]:对象.currentStyle[“属性”]
11.获取可见区域宽高
    在IE中:
        document.body.clientWidth ==> BODY对象宽度
        document.body.clientHeight ==> BODY对象高度
        document.documentElement.clientWidth ==> 可见区域宽度
        document.documentElement.clientHeight ==> 可见区域高度
        document.documentElement.scrollTop =>窗口滚动条滚动高度
    在FireFox中:
        document.body.clientWidth ==> BODY对象宽度
        document.body.clientHeight ==> BODY对象高度
        document.documentElement.clientWidth ==> 可见区域宽度
        document.documentElement.clientHeight ==> 可见区域高度
        document.documentElement.scrollTop =>窗口滚动条滚动高度
    在chrome中:
        document.body.clientWidth ==> BODY对象宽度
        document.body.clientHeight ==> BODY对象高度
        document.documentElement.clientWidth ==> 可见区域宽度
        document.documentElement.clientHeight ==> 可见区域高度
        document.body.scrollTop =>窗口滚动条滚动高度
    在Opera中:
        document.body.clientWidth ==> 可见区域宽度
        document.body.clientHeight ==> 可见区域高度
        document.documentElement.clientWidth ==> 页面对象宽度(即BODY对象宽度加上Margin宽)
        document.documentElement.clientHeight ==> 页面对象高度(即BODY对象高度加上Margin高
    滚动到顶部 window.scrollTo(0,0)
    滚动到尾部 window.scrollTo(0,document.body.clientHeight)

代码练习题

1.变量提升面试题

var a = 100;  
function test(){  
	console.log(a);  
	a = 10;  //去掉了var就变成定义了全局变量了
	console.log(a);  
}  
test();
console.log(a);

2.变量提升面试题

var a = 100;  
function test(){  
	console.log(a);  
	var a = 10;
	console.log(a);  
}  
test();
console.log(a);

3.this指向面试题

var x = 3;
var y = 4;
var obj = {
    x: 1,
    y: 6,
    getX: function() {
        var x =5;
        return function() {
            return this.x;
        }();
    },
    getY: function() {
        var y =7;
        return this.y;
    }
}
console.log(obj.getX())
console.log(obj.getY())

4.this指向面试题

var obj = {
    a: 1,
    b: function(){
        return function(){
            console.log(this) //window
        }
    }
}
var objf = obj.b();
objf();
var obj2 = {
    a: 1,
    b: function(){
        return ()=>{
            console.log(this) //obj2
        }
    }
}
var objf2 = obj2.b();
objf2();

5.this指向面试题

function fun(){
	getName = function(){console.log(1)};
	return this
}
fun.getName = function(){console.log(2)};
fun.prototype.getName = function(){console.log(3)};
var getName = function(){console.log(4)};
function getName(){
	console.log(5);
}

getName();
fun.getName();
fun().getName();
getName();
new fun().getName();

6.eventloop面试题

console.log(1)
new Promise((res) => {
	res();
	console.log(2);
}).then(() => {
	console.log(3)
}).then(() => {
	console.log(4)
})
setTimeout(() => {
	console.log(5)
},0)
console.log(6)
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风舞红枫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值