网站首页 > 基础教程 正文
编辑排版 | 宋大狮
平台运营 | 小唐狮
一、说一下js单线程的理解?
js是单线程的,内部要处理的任务分同步任务、异步任务
异步任务分微任务、宏任务
执行顺序:【又称 事件循环机制 】
先执行同步任务,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中。当所有同步任务执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。
微任务和宏任务有哪些:
宏任务一般是:script、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel
微任务:Promise.then、Object.observe、MutationObserver
示例:
setTimeout(function(){
console.log(1);
});
new Promise(function(resolve){
console.log(2);
resolve();
}).then(function(){
console.log(3);
}).then(function(){
console.log(4)
});
console.log(5);
// 2 5 3 4 1
遇到setTimout,异步宏任务,放入宏任务队列中
遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
Promise.then,异步微任务,将其放入微任务队列中
遇到同步任务console.log(5);输出5;主线程中同步任务执行完
从微任务队列中取出任务到主线程中,输出3、 4,微任务队列为空
从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空
二、说一下defer和async的区别?
相同点:
都是script标签的属性
都是异步加载js
都是为了避免加载脚本的时候就会阻塞页面的渲染,出现空白的现象的问题
不同点:
没有加defer 或 async,同步加载、立即执行、阻塞HTML解析
加defer,异步加载、HTML解析完毕后执行、不阻塞HTML解析
加async,异步加载、立即执行、可能阻塞也可能不阻塞HTML解析
注意:
async和defer属性都仅适用于外部脚本,如果script标签没有src属性,尽管写了async、defer属性也会被忽略
三、说一下浅拷贝和深拷贝的区别?
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
js中基本数据类型存放在栈中,引用数据类型存放在堆中。
浅拷贝是在堆中先创建一个新对象,拷贝原始对象的属性值。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址。
深拷贝是在堆中先创建一个新对象,采用递归方法实现深度克隆原理,将一个对象从内存中完整的拷贝一份出来。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,在堆中创建一个新对象再完整拷贝原对象。
区别:
浅拷贝基本类型之间互不影响,引用类型其中一个对象改变了地址,就会影响另一个对象。
深拷贝改变新对象不会影响原对象,他们之间互不影响。
实现:
浅拷贝:Object.assign()、展开运算符… 、concat()、slice()
深拷贝:JSON.parse(JSON.stringify())、jQuery.extend()、递归
四、说一下数组常用方法有哪些?
push(): 将元素添加到数组的结尾,添加多个用逗号隔开
pop(): 删除数组的最后一项
unshift(): 将元素添加到数组的开头,添加多个用逗号隔开
shift(): 删除数组的第一项
splice(): 删除指定元素,并在删除位置添加元素 --- 3个参数:起始索引 删除数目 新增内容
slice() 截取区间内的元素 --- 2个参数:开始位置 结束位置
fill(): 填充数组 --- 3个参数:值 开始位置 结束位置
-----------------------------------------
String()、toString(): 将数组转成字符串
join(): 把数组转换成字符串,然后给他规定一个连接字符,默认是逗号
Array.from(): 方法用于将两类对象转为真正的数组:类似数组的对象和可遍历的对象(Set 和 Map)
Array.of(): 用于将一组值,转换为数组
-----------------------------------------
map():循环,并且返回新的数组
forEach(): 循环,遍历
filter(): 过滤,筛选出数组中的满足条件的,并且返回新的数组
find(): 查找出第一个符合条件的数组元素
findIndex(): 查找出第一个符合条件的数组元素,所在的索引位置
indexOf():查找数组中值第一次出现的索引
includes(): 查看数组中是否存在此元素
every(): 检测数组中元素是否都是符合条件
some(): 检测数组中元素是否有满足条件的元素
-----------------------------------------
sort(): 对数组排序,正序
reverse(): 对数组进行颠倒
concat(): 合并数组,返回一个新数组
copyWithin(): 指定位置的成员复制到其他位置
五、说一下字符串常用方法有哪些?
replace(): 在字符串中用一些字符替换另一些字符
substr(): 从起始索引提取字符串中指定数目的字符
substring(): 提取字符串中两个指定索引之间的字符
slice() 截取区间内的元素 --- 2个参数:开始位置 结束位置
-----------------------------------------
split(): 把字符串转换成数组,字符串以什么符号连接,就以什么符号分割
-----------------------------------------
charAt(): 返回在指定位置的字符
indexOf(): 返回某个指定的字符串值在字符串中首次出现的位置
lastIndexOf(): 从后向前搜索字符串,并从起始位置(0)开始计算返回字符串最后出现的位置
includes(): 查找字符串中是否包含指定的字符串
search(): 检索字符串中指定的子字符串
startsWith(): 查看字符串是否以指定的字符串开头
-----------------------------------------
toLowerCase(): 把字符串转为小写
toUpperCase(): 把字符串转为大写
trim(): 去掉字符串两边的空白
concat(): 连接两个或多个字符串,返回新的字符串
六、说一下数据类型有哪些?如何判断数据类型?
基本数据类型:Number、String、Boolean、Symbol、Null、Undefined
引用数据类型:Object、Array、Function
typeof:可判断基本数据类型、Function,不能将Object、Array和Null区分,都返回Object
instanceof:可判断Array、Object、Function,不能判断基本数据类型 原理:判断B是否在A的原型链上
Object.prototype.toString.call():可判断所有数据类型
七、说一下数组去重的方法?
1、双重for循环 + splice
for(var i=0;i<arr.length;i++){
for(var j=i+1;j<arr.length;j++){
if(arr[i]===arr[j]){
arr.splice(j,1)
j--
}
}
}
2、filter + indexOf,返回item第一次出现的位置等于当前的index的元素 【常用】
let newArr = arr.filter((item, index) => arr.indexOf(item) === index);
3、filter + Object.hasOwnProperty,利用对象的键名不可重复的特点
let obj = {}
arr.filter(item => obj.hasOwnProperty(typeof item + item) ? false : obj[typeof item])
4、new Set + 扩展运算符 或 Array.from 【常用】
let newArr = [...new Set(arr)];
let newArr = Array.from(new Set(arr));
八、说一下call、apply、bind区别?
ObjA.call(ObjB, argument) --- ObjB调用ObjA中的方法,并把argument作为参数传入
作用:改变this的指向
区别:
1、调用方式不同
call、apply:立即调用,返回的的是一个值
bind:手动调用,返回的是一个函数形式
2、参数不同
三者第一个参数都是调用的对象
call、bind:第二个参数为一个值,多个参数间用逗号分开
apply:第二个参数为一个数组
九、说一下什么是原型、原型链?
原型:
每个构造函数都有一个prototype属性,即 显式原型属性,它指向构造函数的原型对象
每个实例对象都有一个__proto__属性,即 隐式原型属性,它指向构造函数的原型对象
通过显式原型属性向原型对象上设置值,通过隐式原型属性向原型对象上读取值,即 实例对象的隐式原型属性值等于其构造函数的显式原型属性值
原型链:
每个构造函数的原型对象,默认是一个空的Object构造函数的实例对象,也都有一个__proto__属性
实例对象和原型对象的__proto__属性连接起来的一条链,即 原型链,它的尽头是Object构造函数的原型对象的__proto__属性
当在实例对象上读取值时,先在实例对象本身上找,当找不到,再通过__proto__属性,在其构造函数的原型对象上找,
如果还找不到,就继续沿着__proto__属性向上找,直到Object构造函数的原型对象的__proto__属性为止,此时值为null。
十、说一下什么是闭包?
定义在一个函数体内,且访问了外部函数变量 的函数,即 闭包函数(闭包)
优点:
1、延长函数内局部变量的生命周期
2、在函数外部可以操作到函数内部的私有变量【重点作用】
缺点:会导致内存泄露,所以要谨慎使用
应用场景:
1、将函数作为一个函数的返回值 如:封装API模块,只对外暴漏内部的功能函数,不暴漏外部函数的私有变量 --- 类似于类的权限修饰
2、将函数作为实参传递给一个函数内部的另一个函数调用 如:函数内调用定时器函数,将闭包函数作为定时器的回调函数传递
回调和闭包的区别:【是否定义在函数体内】
回调函数:作为实参传递的函数,没有定义在一个函数体内
闭包函数:定义在一个函数体内,且访问了外部函数变量 的函数
举例:
function handle(msg, time) {
setInterval(function callback() {
console.log(msg);
}, time)
}
handle("哈哈哈哈", 5000)
对于setInterval函数来说,callback函数是回调函数;
对于handle函数来说,callback函数是闭包函数;
十一、说一下this的指向?
普通函数:普通函数中this是动态的,在调用函数时确定,一般谁调用指向谁
调用方式 this指向
普通函数调用 window
构造函数调用 实例对象
对象方法调用 该方法所属对象
事件绑定方法 绑定事件的对象
定时器函数 window
立即执行函数 window
箭头函数:
箭头函数没有自己的this、arguments
箭头函数的this是静态的,在定义函数时确定,一般和箭头函数所在父作用域的this指向一致
十二、说一下什么是防抖、节流?
作用:
都是可以限制函数的执行频次,避免函数触发频率过高导致响应速度跟不上触发频率,因而出现延迟、假死或卡顿的现象。
都使用定时器实现
防抖(debounce):【多次重新计时】
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
典型的案例:输入框搜索,输入结束后n秒才进行搜索请求,n秒内又输入内容,则重新计时。
节流(throttle):【多次只一次生效】
规定在一个单位时间内,只能触发一次函数,如果这个单位时间内触发多次函数,只有一次生效。
典型的案例:鼠标不断点击按钮触发事件,规定在n秒内多次点击只生效一次。
十三、说一下ES6新特性有哪些?
新的声明方式,let、const
新的数组方法,如:for of、find()、findIndex()
解构赋值
扩展运算符
箭头函数
模板字符串
symbol数据类型
函数参数允许设置默认值,引入了rest参数(...变量名)
链判断运算符 (?.)
Set集合(伪数组)
Map集合(对象)
Promise
模块化,import、export
类
十四、说一下var、let、const的区别?
var
1、var声明的变量在全局内有效
2、可以重复声明
3、var声明的变量存在变量提升
let
1、遇到{}可开启块级作用域
2、不能重复声明 --- 可以防止变量重复定义产生的冲突,会直接报错
3、let声明的变量不存在变量提升
const
1、const声明的常量是一个只读属性,必须初始化
2、遇到{}可开启块级作用域
3、不能重复声明
4、不存在变量提升
5、const定义的基本数据类型不可以修改,但复杂数据类型可以修改
原因:const指针指向的地址是不可以改变的,但地址指向的内容是可以改变的
十五、说一下for in 和 for of 区别?
for in
可以直接遍历对象,得到属性
遍历数组,得到下标
不能遍历map集合对象
for of
不可以直接遍历对象,因为没有引入iterable,必须加上 Object.key(对象) 才能使用
遍历数组,得到内容
能遍历map集合对象,得到属性和值
十六、说一下对Promise理解?
Promise对象:
描述:
用于封装异步操作并返回其结果的一个构造函数
为了将 异步操作 变 同步操作 执行
三种状态:
pending 待定状态
fulfilled 成功状态
rejected 失败状态
九种方法:
对象上有resolve、reject、all、allSettled、race、any方法
resolve():异步操作成功回调,并将异步操作的结果作为参数传递出去
reject():异步操作失败回调,并将异步操作的结果作为参数传递出去
all():所有的Promise对象均成功后,才会执行all中的then回调,否则返回的是最先rejected状态的值
allSettled():所有的Promise对象均出现结果(无论成功或失败)后,才会执行allSettled中的then回调(只会进入then回调)
any():和all相反,所有的Promise对象均失败后,才会执行any中的失败回调,否则当任意一个Promise对象成功就会直接进入then回调。
race():返回执行最快的一个Promise的结果
原型上有then、catch、finally方法
then():获取成功回调结果,进行逻辑处理
catch(): 获取失败回调结果,抛出异常并处理
finally():无论成功或者失败都会执行
async与await:
出现目的:
await的出现是为了简化多个then链的传参问题
async:
是Promise对象的语法糖,async function A 相当于Promise.resolve(function A)
await:
必须放在async定义的函数内部去使用
作用:
1、等待当前函数执行完毕
2、获取promise中的resolve回调的结果 --- 此处作用同then
异步变同步的解决,经历了四个阶段:
1、回调函数
描述:
回调里面嵌入回调,导致层次很深,代码维护起来特别复杂,产生回调地狱问题
示例代码:
getData(){
//获取分类列表id
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/navlist.php",
success:res=>{
let id=res.data[0].id
// 根据分类id获取该分类下的所有文章
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/newslist.php",
data:{
cid:id
},
success:res2=>{
//获取到一篇文章的id,根据文章id找到该文章下的评论
let id=res2.data[0].id;
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/comment.php",
data:{
aid:id
},
success:res3=>{
//找到该文章下所有的评论
console.log(res3)
}
})
}
})
}
})
}
2、回调函数封装
描述:
把每一个request请求封装出一个函数,将结果进行返回
代码条理清晰了,但还是回调里面嵌套回调,并没有解决回调地狱的问题
示例代码:
//在onload初始化后调用相应的函数
onLoad() {
//调用导航函数,并拿到函数的返回值
this.getNav(res=>{
let id=res.data[0].id;
//拿到分类id作为参数
this.getArticle(id,res2=>{
//拿到文章id作为参数
let id=res2.data[0].id;
this.getComment(id,res3=>{
//最终获取到第一个分类下,第一篇文章下,所有评论
console.log(res3)
})
})
});
}
methods: {
//先获取导航分类接口,将结果进行返回,到调用函数的地方获取
getNav(callback){
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/navlist.php",
success:res=>{
callback(res)
}
})
},
//获取文章数据,将文章列表进行返回
getArticle(id,callback){
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/newslist.php",
data:{
cid:id
},
success:res=>{
callback(res)
}
})
},
//获取文章下的所有评论
getComment(id,callback){
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/comment.php",
data:{
aid:id
},
success:res=>{
callback(res)
}
})
}
}
3、promise then
示例代码:
//promise链式调用
this.getNav()
.then(res=>{
let id=res.data[0].id;
return this.getArticle(id);
})
.then(res=>{
let id=res.data[0].id;
return this.getComment(id)
})
.then(res=>{
console.log(res)
})
methods: {
//先获取导航分类接口,将结果进行返回,到调用函数的地方获取
getNav(callback){
return new Promise((resolve,reject)=>{
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/navlist.php",
success:res=>{
resolve(res)
},
fail:err=>{
reject(err)
}
})
})
},
//获取文章数据,将文章列表进行返回
getArticle(id){
return new Promise((resolve,reject)=>{
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/newslist.php",
data:{
cid:id
},
success:res=>{
resolve(res)
},
fail:err=>{
reject(err)
}
})
})
},
//获取文章下的所有评论
getComment(id){
return new Promise((resolve,reject)=>{
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/comment.php",
data:{
aid:id
},
success:res=>{
resolve(res)
},
fail:err=>{
reject(err)
}
})
})
}
}
4、promise await
描述:
await / async 这两个命令是成对出现的,如果使用await没有在函数中使用async命令,那就会报错,如果直接使用async没有使用await不会报错,只是返回的函数是个promise
示例代码:
async onLoad() {
let id,res;
res=await this.getNav();
id=res.data[0].id;
res=await this.getArticle(id);
id=res.data[0].id;
res=await this.getComment(id);
console.log(res)
}
methods: {
//先获取导航分类接口,将结果进行返回,到调用函数的地方获取
getNav(callback){
return new Promise((resolve,reject)=>{
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/navlist.php",
success:res=>{
resolve(res)
},
fail:err=>{
reject(err)
}
})
})
},
//获取文章数据,将文章列表进行返回
getArticle(id){
return new Promise((resolve,reject)=>{
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/newslist.php",
data:{
cid:id
},
success:res=>{
resolve(res)
},
fail:err=>{
reject(err)
}
})
})
},
//获取文章下的所有评论
getComment(id){
return new Promise((resolve,reject)=>{
uni.request({
url:"https://ku.qingnian8.com/dataApi/news/comment.php",
data:{
aid:id
},
success:res=>{
resolve(res)
},
fail:err=>{
reject(err)
}
})
})
}
}
十七、说一下伪数组转为真数组的方法?
1、Array.prototype.slice.call(伪数组)
2、Array.from(伪数组)
3、剩余运算符...
十八、说一下三种缓存方式的区别?
cookie:用来保存登录信息,大小限制为4KB左右
localStorage:用于本地数据存储,保存的数据没有过期时间,一般浏览器大小限制在5MB
sessionStorage:接口方法和localStorage类似,但保存的数据的只会在当前会话中保存下来,页面关闭后会被清空
十九、说一下创建Ajax的基本步骤?
1、创建XMLHttpRequest对象
2、创建http请求
3、发送http请求
4、设置http请求状态变化的函数
5、获取服务器返回的数据
示例代码:
// 1
const xhr = new XMLHttpRequest();
// 2
xhr.open('POST', "http://localhost:xxx");
// 3
xhr.send("a=100&b=200");
// 4
xhr.onreadystatechange = function(){
if(xhr.readyState==4){
// 5
if(xhr.status >= 200 && xhr.status < 300){
result.innerHTML = xhr.response;
}
}
}
二十、说一下HTTP与HTTPS的区别?
1、HTTP是超文本传输协议,信息是明文传输,HTTPS是由SSL协议 + HTTP协议,信息是加密传输
2、HTTPS协议需要花钱申请证书,免费证书少
3、HTTP和HTTPS默认使用的端口不同,前者是80,后者是443
二十一、说一下请求方式post和get的区别?
1、安全性
get请求参数会被拼接到地址栏上,信息会暴露
post请求参数不可见
2、数据传输量
get有长度限制
post不会
3、缓存
get数据会被缓存
post不会
4、后端的习惯
查用用get,因为要分页,有长度限制
增删改用post
二十二、说一下常见的http状态码?
4xx表示客户端错误
401表示请求格式错误
402表示请求未授权
403表示禁止访问
404表示请求的资源不存在,一般是路径写错了
5xx表示服务器错误
500表示最常见的服务器错误,一般是前端参数传错了、或后端代码写错了
503表示服务器构建
二十三、说一下js的模块化?
作用:
一个模块就是实现某个特定功能的文件,在文件中定义的变量、函数、类都是私有的,对其他文件不可见。
为了解决引入多个js文件时,出现 命名冲突、污染作用域 等问题
AMD:
浏览器端模块解决方案
AMD即是“异步模块定义”
在AMD规范中,我们使用define定义模块,使用require加载模块
提前执行:它采用异步方式加载模块,一边加载一边执行
依赖前置:依赖必须在定义时引入
CMD:
浏览器端模块解决方案
CMD即是“通用模块定义”
在CMD规范中,我们使用define定义模块,使用require加载模块
延迟执行:它采用异步方式加载模块,先加载完毕再按需执行
依赖就近:依赖可以在代码的任意一行引入
CommonJS:
服务器端模块解决方案
在CommonJS规范中,我们使用module.exports导出模块,使用require加载模块
立即执行:它采用同步方式加载模块,先加载后执行,执行完毕会被缓存
依赖就近:依赖可以在代码的任意一行引入
ESModule:
浏览器端 和 服务器端 通用的模块解决方案
在ESModule规范中,我们使用export导出模块,使用import加载模块
延迟执行:它采用异步方式加载模块,先加载完毕再按需执行
依赖就近:依赖可以在代码的任意一行引入
二十四、说一下DOM的操作有哪些?
查:
document.querySelector('选择器') --- 获取单个节点
document.querySelectorAll('选择器') --- 获取多个节点,伪数组
parentNode:获取父节点
children: 获取子节点 --- 伪数组
nextElementSibling:获取下一个兄弟
previousElementSibling:获取上一个兄弟
增:
appendChild:添加节点到最后
insertBefore:在某个元素前面插入
cloneNode:克隆节点
删:
removeChild:删除子节点
remove:删除节点
改:
replaceChild:修改子节点
属性操作:
getAttribute:获取属性
setAttribute:设置属性
内容操作:
innerHTML:获取/设置代码内容
innerText:获取/设置文本内容
二十五、说一下对象创建模式有哪些?
对象字面量(花括号)
工厂模式(对象字面量 + return新对象)
Object构造函数(new Object)
构造函数模式(new function + 属性、方法都在构造函数上)
原型模式(new function + 属性、方法都在原型上)
组合模式(属性在构造函数上 + 方法在原型上)
类(底层就是对 组合模式 进行了封装)
二十六、说一下对象继承模式有哪些?
原型链继承(子类原型指向父类实例)
构造函数继承(借助 call)
组合继承(原型链继承 + 构造函数继承)
原型式继承(借助 Object.create)
寄生式继承(原型式继承 + 添加子类方法)
寄生组合继承(寄生式继承 + 组合继承)
extends(底层就是对 寄生组合继承 进行了封装)
二十七、说一下执行上下文的理解?
在 代码执行前 产生
产生变量提升、函数提升的原因
定义:
全局执行上下文对象:在执行全局代码前,创建对应的全局执行上下文对象,即window对象,进行预处理
函数执行上下文对象:在调用函数后、准备执行函数体之前,创建对应的函数执行上下文对象,进行预处理
块级私有执行上下文对象:在执行块级代码前,创建对应的块级私有执行上下文对象,进行预处理
执行上下文栈:
存放执行上下文对象的栈
按照上下文对象创建的次序进栈,然后从栈顶依次执行出栈
二十八、说一下什么是作用域、作用域链?
在 代码编写时 产生
定义:
全局作用域:全局执行上下文对象的有效作用范围
函数作用域:函数执行上下文对象的有效作用范围
块作用域:块级私有执行上下文对象的有效作用范围
作用域链:
在某一作用域内找某一变量时,先在自身作用域内的执行上下文对象中找,找不到再去父作用域内的执行上下文对象中找,依次向上找,直到全局作用域内的执行上下文对象为止。这个过程称为作用域链。
- END -
猜你喜欢
- 2024-12-22 一大波开源小抄来袭 开源小说软件下载
- 2024-12-22 「Electron跨平台桌面应用开发 4」系统托盘功能
- 2024-12-22 超级简单 Bing美图每天自动收 收藏美图
- 2024-12-22 使用Python进行并发编程 python 并发编程
- 2024-12-22 我的世界计分板命令创建队伍教程详解
- 2024-12-22 Webpack5 配置手册(从0开始) webpack简单配置
- 2024-12-22 electron开发桌面应用实现串口通信,看完你就学会了
- 2024-12-22 JavaScript中原生的原型 Prototype
- 2024-12-22 webpack系列学习-基本用法 webpack基本使用
- 2024-12-22 前端技术探秘-Nodejs的CommonJS规范实现原理
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)