前端面试之js基础
什么是闭包
能够读取其他函数内部变量的函数。
或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用
例子
function fn(){
var a=2
return function(){
console.log(a)//2
}
}
优点
隐藏变量以及防止变量被篡改和作用域的污染,从而实现封装
缺点
函数执行完后,函数内的局部变量没有释放,占用内存时间变长,容易造成内存泄漏
解决方法:
让内部函数编程垃圾对象,赋值为null,即时释放,让浏览器回收闭包
闭包的用途
- 1.读取函数内部的变量
- 2.让这些变量的值始终保持在内存中。不会在f1调用后被自动清除
- 3.方便调用上下文的局部变量。利于代码封装
原因:f1是f2的父函数,f2被付给了一个全局变量,f2始终存在内存中,f2的存在依赖f1,因此f1也始终存在内存中,不会在调用结束后,被垃圾回收机制回收
闭包的应用
防抖
<body>
<input type="text" id='unDebounce'>
</body>
</html>
<script>
//防抖函数
function _debounce(fn, delay) {
var delay = delay || 200;
var timer;
return function () {
var th = this;
var args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
//timer = null; 会自己null,settimeout只执行一次
fn.apply(th, args);
}, delay);
};
}
//模拟一段ajax请求
function ajax(content){
console.log('ajax request ' + content)
};
let inputa = document.getElementById('unDebounce');
function fn(e){ ajax(e.target.value) }
//防抖函数,处理多次被触发的事件,只执行最后一次
inputa.addEventListener('input', _debounce(fn,1000))
</script>
闭包的两道经典题
题目一:
var name = "the window";
var obj = {
name : "myObject", // 属性
getNameFunc:function(){ // 方法
return function(){
return this.name;
};
}
};
alert(obj.getNameFunc()()); // 输出了 “the window”;
答案: 输出了全局 the window,为什么不是对象属性 myObject 呢?
解法: 形成一个闭包环境,需要两个条件:函数嵌套,子函数引用父函数局部变量,但是上面的例子,我只看到了函数嵌套,但没看到子函数引用父函数局部变量,所以这个函数嵌套根本就产生不了闭包环境,所以他不是个闭包。
调用 alert(obj.getNameFunc()()),注意,他有两个 () () ,调用第一个 () 时,实际上就是执行了getNameFunc()这个函数,但这个函数又嵌套了 return function(){ return this. name },
所以执行第一个括号时,就是执行了 return function(){ return this. name },到执行第二个 () 时,才是真正执行到了第二个函数的内部了,也就是 return this.name 了,但由于这个函数方法,他不是个闭包环境,所以 this 指向了 window 了,也就是 var name = “the winodw” ;
题目二:
var name2 = "the window";
var obj2 = {
name2:"my name is a obj",
getNameFunc2 :function fn1(){
var that = this;
return function(){
return that.name2;
};
}
}
alert(obj2.getNameFunc2()()); // 输出 my name is a obj
这次是输出的是 对象内的属性了,因为这次的才是真正的闭包环境,有函数嵌套,有父函数变量给子函数引用,形成依赖关系,产生闭包环境。
父函数明确定义了局部变量,var that = this;然后这个 that 呢,他又被子函数所引用,当 alert(obj2.getNameFunc()()) 执行时,就是执行子函数内部的 return that.name,
先在自身查找有没有 that.name,没有就返回上一层父函数,找到了 var that = this 了,这个就是闭包的环境。当然,两个函数执行域内都没有这个变量属性时,再往上一层,
也就是obj2对象内的属性,有个 name,终于在对象的执行域找到了,那就输出了 对象name的属性值。
JS有哪些数据类型?
根据JS中的变量类型传递方式,分为基本数据类型和引用数据类型两大类共七种。
基本数据类型包括
Undefined Null Boolean Number String Symbol Bigint 七种
symbol(唯一值) ES6中新增的数据类型(不能被new) 创建唯一值Symbol(10)===Symbol(10):false
bigint(大数据值) ES6中新增的数据类型(不能被new) Number.MAX_SAFE_INTEGER(最大安全数)
引用数据类型只有Object一种,主要包括对象,数组和函数。(还有正则和日期)
基本数据类型和引用数据类型有什么区别?
- 1.两者作为函数的参数进行传递时:
基本数据类型传入的时数据的副本,原数据的更改不会影响传入后的数据。
引用数据类型传入的时数据的引用地址,原数据的更改会影响传入后的数据 - 2.两者在内存中的存储位置:
基本数据类型存储在栈中
引用数据类型在栈中存储了指针,该指针指向的数据实体存储在堆中
判断数据类型的方法有哪些?
- 1.利用typeof可以判断数据的类型
- 2.A instanceof B可以用来判断A是否为B的实例,但它不能检测null和undefined
- 3.B.constructor===A 可以判断A是否为B的原型,但constructor检测object与instanceof不一样,还可以处理基本数据类型的检测
不过函数的constructor是不稳定的,这个主要体现在把类的原型进行重写,在重写的过程中很有可能出现把之前的constructor给覆盖了,这样检测出来的结果就是不准确的。
- 4.Object.prototype.toString.call() 这个最准确
浅拷贝与深拷贝有何区别?如何实现?
浅拷贝只复制指向某个对象的指针,而不复制对象本身。浅拷贝的实现方式有:
- 1.Object.assign():需注意的是目标对象只有一层的时候,是深拷贝
如果是字符串将自动被转为对象 - 2.扩展运算符
深拷贝就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份,深拷贝的实现方式有: - 1.手写遍历递归赋值
function copyObj(obj){
let newObj={}
for(let key in obj){
if(typeof obj[key] =='object'){
newObj[key]=copyObj(obj[key])
}else{
newObj[key]=obj[key]
}
}
return newObj
}
- 2.结合使用JSON.parse()和JSON.stringify()方法
let,const的区别是什么?
let与const的区别是什么?
let与const都是旨在声明所在的块级作用域内有效
let声明的变量可以改变,值和类型都可以改变,没有限制
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值
对于复合类型的变量,如数组和对象,变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
const names=[]
names=[1,2,3] //报错,地址改变了
names[0]=1
names[1]=2
names[2]=3
如何将对象添加的新对象进行冻结呢?
object.freeze(names)
var constantize(obj)=>{
Object.freeze(obj)
Object.keys(obj).forEach(key=>{
if(typeof obj[key] ==='object'){
constantize(obj[key])
}
})
}
什么是执行上下文和执行栈?
变量或函数的执行上下文,决定了它们的行为以及可以访问哪些数据。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量都存在于这个对象上(如DOM中全局上下文关联的便是window对象)
简而言之,JS中运行任何的代码都是在执行上下文中运行
每个函数调用都有自己的上下文,当代码执行流进入函数时,函数的上下文被推到一个执行栈中。在函数执行完之后,执行栈会弹出该函数上下文,在其上的所有变量和函数都会被销毁,并将控制权返还给之前的执行上下文。JS的执行流就是通过这个执行栈进行控制的。
作用域和执行上下文的区别是什么?
- 1.函数的执行上下文只在函数被调用时生成,而其作用域在创建时已经生成
- 2.函数的作用域会包含若干个执行上下文(有可能时零个,当函数未被调用时)
this指向的各种情况都有什么?
this的指向只有在调用时才能被确定,因为this时执行上下文的一部分
- 1.全局作用域中的函数:其内部this指向window
var a=1
function fn(){
console.log(this.a)
}
fn()
- 2.对象内部的函数:其内部this指向对象本身
var a=1
var obj={
a:2,
fn:function(){
console.log(this.a)
}
}
obj.fn() //2
- 3.构造函数:其内部this指向生成的实例
function createP(name,age){
this.name=name
this.age=age
}
var p=new createP("老李",46)
- 4.有apply,call,bind改造的函数:其this指向第一个参数
function add(c,d){
return this.a+this.b+c+d
}
var o={a:1,b:2}
add.call(o,5,7) //15
- 5.箭头函数 没有自己的this,arguments,super,new,target(匿名函数)
如何改变this指针的指向?
如何理解同步和异步?
同步:按照代码书写顺序一一执行处理指令的一种模式,上一段代码执行完才能执行下一段代码。
异步:可以理解为一种并行处理的方式,不必等待一个程序执行完,可以执行其他的任务。
JS之所以需要异步的原因在于JS是单线程运行的。常用的异步场景有:定时器,ajax请求,事件绑定
JS是如何实现异步的?
JS引擎是单线程的,但又能实现异步的原因在于事件循环和任务队列体系。
- 事件循环:JS会创建一个类似于while(true)的循环,每执行一次循环体的过程之为Tick。每次Tick的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次Tick会查看任务队列中是否有需要执行的任务。
- 任务队列:异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如onclick,setTimeout,ajax处理的方式都不同,这些异步操作是由浏览器内核的webcore来执行的,浏览器内核包含3忠webAPI,分别是DOM Binding,network,timer模块
onclick由DOM Binding模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中
setTimerout由timer模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中
ajax由network模块来处理,在网络请求完成返回之后,才将回调函数添加到任务队列中
- 主线程:JS只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后才开始执行的。
所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。只有当主线程中执行栈为空的时候,才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
什么是ajax?如何实现?
ajax是一种能够实现局部刷新网页的技术,可以使网页异步刷新
ajax的实现主要包括四个步骤:
- 1.创建核心对象XMLhttpRequest(异步调用对象)
var xmlHttp
if(window.XMLHttpRequest){
xmlHttp=new XMLHttpRequest()//IE6 以外的浏览器
}else{
xmlHttp=new ActiveXObject("Microsoft.XMLHTTP") //IE5 IE6
}
- 2.利用open方法打开与服务器的连接
xmlhttp.open(method,url,async);
- 3.利用send方法发送请求(“POST”请求时,还需额外设置请求头)
xmlhttp.send();
- 4.监听服务器响应,接收返回值
var xmlHttp = new XMLHttpRequest();
xmlHttp.open('get','demo_get.html','true');//调用open()方法并采用异步方式
xmlHttp.send(); //使用open()方法将请求发送出去
xmlHttp.onreadystatechange()=>{
if(xmlHttp.readyState === 4 && xmlHttp.status === 200){
}
}
readyState
readyState | 含义 |
---|---|
0 | (未初始化): (XMLHttpRequest)对象已经创建,但还没有调用open()方法。 |
1 | (载入):已经调用open() 方法,但尚未发送请求。 |
2 | (载入完成): 请求已经发送完成。 |
3 | (交互):可以接收到部分响应数据。 |
4 | (完成):已经接收到了全部数据,并且连接已经关闭 |
实现异步的方式有哪些?
- 1.回调函数模式:将需要异步执行的函数作为回调函数执行,其缺点在于处理复杂逻辑异步逻辑时,会造成回调地狱(回调嵌套层数太多,代码结构混乱)
- 2.事件监听模式:采用事件驱动的思想,当某一事件发生时触发执行异步函数,其缺点在于整个代码全部得变为事件驱动模式,难以分辨主流程。
- 3.发布订阅模式:当异步任务执行完成时发布消息给信号中心,其他任务通过在信号中心中订阅消息来确定自己是否开始执行
- 4.Promise(SE6):Promise对象共有三种状态pending(初始化状态),fulfilled(成功状态),rejected(失败状态)
- 5.async/await(SE7):基于Promise实现得异步函数
- 6.利用生成器实现
怎么理解promise对象?
简单来说,是一个容器,比传统的异步编程更合理更强大
promise对象有如下两个特点:
- 1.对象的状态不受外界影响。Promise对象共有三种状态pending,fulfilled,rejected。状态值只会被异步结果决定,其他任何操作无法改变。(三个状态)
- 2.状态一旦成型,就不会再变,且任何时候都可得到这个结果。状态值会由pending变为fulfilled或rejected,这时即为resolved(两个过程)
promise的三个缺点
- 1.Promise一旦执行便无法被取消
- 2.不可设置回调函数,其内部发生的错误无法捕获
- 3.当处于pending状态时,无法得知其具体发展到哪个阶段
promise中常用的方法有:
- 1.Promise.prototype.then():promise实例的状态发生改变时,会调用then内部的回调函数。then方法接受两个参数(第一个为resolved状态时执行的回调,第二个为rejected状态时执行的回调)
- 2.promise.prototype.catch():.then(null,rejection) 或 .then(undefined,rejection)的别名,用于指定发生错误时的回调函数。
实例
当我们构造promise的时候,构造函数内部的代码是立即执行的
new Promise((resolve, reject) => {
console.log('new Promise')
resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh
promise实现了链式调用,也就是说每次调用then之后返回的都是一个promise,并且是一个全新的promise,原因也是因为状态不可变。如果你再then中使用了return,那么return的值会被promise.resolve()包装
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包装成 Promise.resolve(2)
})
.then(res => {
console.log(res) // => 2
})
promise很好地解决了回调地狱的问题
promise基础
1.下面代码的执行结果是
const promise=new Promise((resolve,reject)=>{
console.log(1);
console.log(2);
})
promise.then(()=>{
console.log(3);
})
console.log(4);//1 2 4
最终应该输出1 2 4.这样最主要的就是3,要知道promise.then是微任务,会在所有的宏任务执行完之后才会执行,同时需要promise内部的状态发生变化,因为这里内部没有发生变化,所以不输出3
2.下面代码的执行结果
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);
输出结果如下:
'promise1'
'1' Promise{<fulfilled>: 'resolve1'}
'2' Promise{<pending>}
'resolve1'
如果直接打印promise1,会打印出它的状态值和参数。
思路如下:
- script是一个宏任务,按照顺序执行这些代码
- 首先进入Promise,执行构造函数中的代码,打印
promise1
- 碰到
resolve
函数,将promise1
的状态改变为resolved
,并将结果保存下来 - 碰到
promise1.then
这个微任务,将它防如微任务队列 promise2
是一个新的状态为pending
的Promise
- 执行同步代码1,同时打印出
promise1
的状态是resolved
- 执行同步代码2,同时打印出
promise2
的状态是pending
- 宏任务执行完毕,查找微任务队列,发现
promise1.then
这个微任务且状态为resolved
,执行它,输出了resolve1
3.下面代码的执行结果是
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
输出结果如下:
1
2
4
timerStart
timerEnd
success
- 首先遇到Promise构造函数,会先执行里面的内容,打印1
- 遇到
setTimeout
,它是一个宏任务,被推入宏任务队列 - 按下继续执行,打印出2
- 由于
promise
的状态此时还是pending
,所以promise.then
先不执行 - 继续执行下面的同步任务,打印出4
- 微任务队列此时没有任务,继续执行下一轮宏任务,执行
setTimeout
- 首先执行
timerStart
,然后遇到了resolve
,将promise
的状态改为resolved
且保存结果并将之前的promise.then
推入微任务队列,再执行timerEnd
- 执行完这个宏任务,就去执行微任务
promise.then
,打印出resolve
的结果
4.下面代码的执行结果是
Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('start');
- 首先,Promise.resolve().then是一个微任务,加入微任务队列
- 执行timer1,它是一个宏任务,加入宏任务队列
- 继续执行下面的同步代码,打印出start
- 这样第一轮的宏任务就执行完了,开始执行微任务,打印出promise1
- 遇到timer2,它是一个宏任务,将其加入宏任务队列
- 这样第一轮的微任务就执行完了,开始执行第二轮宏任务,执行是定期timer1,打印timer1
- 遇到Peromise,它是一个微任务,加入微任务队列
- 开始执行微任务队列中的任务,打印promise2
- 最后执行宏任务timer2定时器,打印出timer2
start
promise1
timer1
promise2
timer2
5.下面代码的执行结果
const promise = new Promise((resolve, reject) => {
resolve('success1');
reject('error');
resolve('success2');
});
promise.then((res) => {
console.log('then:', res);
}).catch((err) => {
console.log('catch:', err);
})
输出结果如下
then:success1
promise的状态发生变化之后,就不会再发生变化。开始状态由pending变为resolve,说明已经变为已完成状态,下面的两个状态的就不会再执行,同时下面的catch也不会捕获到错误。
6.下面代码的执行结果是
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
输出结果如下:
promise1 Promise {<pending>}
promise2 Promise {<pending>}
Uncaught (in promise) Error: error!!!
promise1 Promise {<fulfilled>: "success"}
promise2 Promise {<rejected>: Error: error!!}
promise的catch,then,finally
7.下面代码的执行结果是
Promise.resolve(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});
输出结果1 2
promise可以链式调用,因为每次调用.then或者.catch都会返回一个新的promise,从而实现了链式调用,它并不像一般我们任务的链式调用一样return this。
上面输出结果之所以一次打印出1和2,是因为resolve(1)之后走的第一个then方法,并没有走catch里,所以第二个then中的res得到的实际上是第一个then的返回值。并且return 2会被包装成resolve(2),被最后的then打印输出2
8.下面代码的执行结果是
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
输出结果:1
一个原则:.then
和.catch
的参数期望是函数,传入非函数则会发生值透传。第一个then和第二个then中传入的都不是函数,一个数字,一个是对象,因此发生了透传,将resolve(1)
的值直接传到最后一个then里。
9.下面代码的执行结果是
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}, (err) => {
console.log('error', err)
}).catch(err => {
console.log('catch', err)
})
.then
函数中的两个参数:
- 第一个参数是用来处理Promise成功的函数
- 第二个则是处理失败的函数
结果==‘error’ ‘error!!!’==
10.下面代码的执行结果
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
.finally()一般用的很少,只要记住以下几点就可以了:
- .finally()方法不管Promise对象最后的状态如何都会执行
- .finally()方法的回调函数不接受任何的参数,也就是说你再.finally()函数中是无法知道Promise最终的状态是fulfilled还是rejected
- 它最终返回的默认回事一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象
- finally本质上是then方法的特例
Async,await
11.下面代码的执行结果是
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
console.log('start')
输出结果如下
async1 start
async2
start
async1 end
- 首先执行函数中的同步代码
async1 start
,之后碰到了await
,它会阻塞async1
后面代码的执行,因此会先去执行async2
中的同步代码async2
,然后跳出async1
- 跳出async1函数后,执行同步代码
start
- 在义论宏任务全部执行完之后,再来执行
await
后面的内容async1 end
12.下面代码的执行结果是
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
console.log('script end')
结果如下:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
怎么理解宏任务,微任务?
宏任务:script(整体代码,setTimerout,setInterval,I/O,页面渲染)
微任务:Promise.then,Object.observe,MutationObserver
执行顺序如下:
主线程任务----宏任务----微任务----微任务里的宏任务----…
实现继承的方法有哪些?
class+extends继承(ES6)
class Animal{
constructor(name){
this.name=name
}
}
class Cat extends Animal{
constructor(name){
super(name)
}
eat(){
console.log("eating");
}
}
原型继承
prototype
function Animal(name){
this.name=name
}
Animal.prototype.eat=function(){
console.log("eating");
}
function Cat(furColor){
this.color=color
}
Cat.prototype=new Animal()
借用构造函数继承
function SuperType(){
this.colors=['red','blue','green']
}
function SubType(){
SuperType.call(this)
}
var instance1=new SubType()
instance1.colors.push('black')
console.log(instance1.colors);
var instance2=new SubType()
console.log(instance2.colors);
借用构造函数的优势
可以在子类型结构中向超类型构造函数传递参数
function SuperType2(name){
this.name=name
}
function SubType2(){
SuperType2.call(this,"123")
this.age=29
}
var instance2=new SubType2()
console.log(instance2.name);
console.log(instance2.age);
寄生组合式继承
就是既有构造函数继承又有原型继承
require/import之间的区别?
- 1.require是CommonJS语法,import是ES6的语法
- 2.require是在后端服务器支持,import在高版本浏览器及Node中都可以支持
- 3.require引入的是原始导出值的复制,import则是导出值的引用
- 4.require是运行时动态加载,import时静态编译
- 5.require调用时默认不是严格模式,import则默认调用严格模式
原型和原型链
- 1.原型:JavaScript的对象中都包含乐一个[proto]内部属性,这个属性所对应的就是自身的原型
- 2.原型链:当一个对象调用自身不存在的属性/方法时,就会去自己[proto]关联的前辈prototype对象上去找,如果没找到,就回去该prototype原型[proto]关联的前辈prototype去找。依次类推,知道找到或undefined为止。从而形成乐所谓的“原型链”
事件委托
事件委托,又名事件代理。事件委托就是利用事件冒泡,就是把子元素的事件都绑定到父元素上。如果子元素阻止乐事件冒泡,那么委托也就没法实现了。
解释一下变量的提升
变量的提升是JavaScript的默认行为,这意味着将所有变量声明移动到当前作用域的顶部,并且可以在声明之前使用变量。初始化不会被提升,提升仅作用于变量的声明。
简单来说就是,声明会自动放在最前面
a='123'
var a
console.log(a);
等价于
var a
a='123'
console.log(a);
例子1
var getName=function(){
console.log('4');
}
function getName(){
console.log('5')
}
getName()
- 1.
getName
函数和变量getName
被提升 - 2.然后
var getName
因为重复声明被忽略 - 3.最后函数表达式
getName
会覆盖getName
所以最后实际执行的是getName=function() … 结果是4
例子2
function showName() {
console.log('name1');
}
showName();
function showName() {
console.log('name2');
}
showName();
同理 name2 name2
如何理解高阶函数
JavaScript中的一切都是对象,包括函数。我们可以将变量作为参数传递给函数,函数也是如此。我们调用接受和返回另一个函数称为高阶函数的函数
简单来说就是,函数里还有函数
如何区分声明函数和表达式函数
两个函数将在不同的时期定义。在解析期间定义声明,在运行时定义表达式。