大家好!欢迎来到咱们 JS 脚本语言的第五堂课。
上节课呢,咱们算是跟“函数”这个新朋友打了个招呼,知道了怎么调用它,怎么给它传参数,还有它最后能吐出什么返回值。那今天咱们就要往深了挖一挖,讲讲返回值的一些“隐藏玩法”,以及 JS 里特别有名的一个概念——闭包。
先说返回值。上节课咱们讲了,函数能返回字符串、数字、数组、对象这些。但你们知道吗?它其实还能“返回另一个函数”!这就好比你去餐厅点了一份“神秘套餐”,服务员端给你的不是菜,而是一张“兑换券”,你得拿着这张券再去换真正的菜。
咱们来看个最简单的例子:
function greet() {
return function() {
console.log('Hello!');
};
}
let sayHello = greet();
sayHello();
你看啊,咱们调用 greet 的时候,它并没有直接打印 Hello,而是吐出来一个“还没被执行的函数”。咱们用 sayHello 这个变量把这个函数接住,然后再像普通函数那样去调用 sayHello(),这时候控制台才打印出 Hello。这就说明,函数当返回值是完全没毛病的,用起来也跟普通函数一模一样。
这时候你可能要问了:“干嘛这么麻烦?直接写个普通函数不行吗?”这就引出了咱们今天的一个重头戏——“闭包”。
闭包听起来挺高大上,其实用大白话讲,就是“函数记住了自己出生的老家”。当一个函数被返回到外面的时候,它不仅仅是把代码带出来了,它还把老家(也就是它定义时所在的环境)里的变量也一起打包带走了。哪怕外面的大函数已经执行完、下班了,这些变量也不会被销毁,而是被这个带出来的内部函数继续攥在手里。
咱们看个计数器的例子就明白了:
function createCounter() {
let count = 0;
count++;
console.log(count);
let counter = createCounter();
counter();
按理说,createCounter 执行完后,里面的 count 变量就该被系统回收了。但是!因为我们返回的那个小函数还在用着 count,所以系统就不敢回收它。于是每次调用 counter(),它都能在上一次的基础上接着加 1。这就是闭包最核心的本事:内部函数能一直抓着外部函数的变量不放,让变量的寿命变长了。
那闭包除了能当计数器,还有啥用呢?它最牛的地方在于能搞“私有化”。啥叫私有化呢?就像你把自己的日记本锁在抽屉里,别人想看你得通过你给的钥匙,不能直接翻。
咱们看这段代码:
function createPerson(name) {
let age = 0;
return {
setName: function(newName) {
name = newName;
},
getName: function() {
return name;
growUp: function() {
age++;
getAge: function() {
return age;
let person = createPerson('小明');
console.log(person.getName());
person.setName('小红');
person.growUp();
console.log(person.getAge());
在这个例子里,name 和 age 就是被藏起来的“私有变量”。你没办法直接写个 person.age = 100 去改年龄,只能通过我们给的 growUp 方法让它一岁一岁长。这就像给数据装了一道防盗门,所有操作都得走正规渠道,程序自然就安全、可控多了。
这里给大家提个醒,闭包记住的是那个变量“本身”,而不是它当时的“照片”。如果外面有好几个内部函数,它们共享的是同一个变量。比如:
function createMulti() {
let x = 0;
add: function() {
x++;
show: function() {
return x;
let obj = createMulti();
obj.add();
obj.show();
你看,add 和 show 盯上的都是同一个 x,add 把 x 加了两次,show 去读的时候,看到的就是 2。
那怎么判断代码里有没有闭包呢?记住一个死理儿:如果一个函数用了它外部的变量,并且这个函数被扔到外面去执行了,那闭包就产生了。最常见的套路就是“函数里返回函数”。
不过闭包有个小毛病,就是容易“占着茅坑不拉屎”。因为变量一直被引用,系统就不会回收它。如果你搞了太多闭包又不注意清理,内存就会被撑爆,也就是所谓的“内存泄漏”。所以不用闭包的时候,记得把外面的变量赋个 null,让内部函数找不到它,系统就能把内存回收了。
好啦,闭包咱们就聊到这。接下来咱们看看 JS 里的几个“关键字”。这些关键字是 JS 引擎自带的,用好了能事半功倍。
先说 function。咱们之前用它来声明函数,但它还能用来搞“函数表达式”,也就是直接把一个匿名函数塞给一个变量:
let sayHi = function() {
console.log('Hi!');
这里要注意个坑:用 function 直接声明的函数,系统会“提前打招呼”(也就是变量提升),你在代码前面调用它都没事;但如果是上面这种赋值给变量的写法,系统是不会提前打招呼的。你要是写在定义前面去调用,就会报错,因为那时候 sayHi 还是个 undefined(空壳子)。
再说 return。它是用来告诉函数“到此为止,带着东西下班”的。一旦碰到 return,后面的代码就不执行了。这里有个超级容易踩的坑:return 后面千万别直接换行!因为 JS 有个自作聪明的机制,会在换行的地方自动加分号。比如:
function foo() {
return
1;
你以为它返回 1,其实它返回的是 undefined。所以如果你想返回一个对象或者一长串东西,记得把大括号或者内容跟 return 写在同一行。
最后说说 this。this 是个“变色龙”,它的值取决于函数是怎么被叫出来的,而不是它写在哪。比如:
let user = {
name: '小明',
say: function() {
console.log(this.name);
user.say();
这里 say 是作为 user 的小弟(方法)被叫出来的,所以 this 指向 user,能打印出“小明”。但如果你把它单独拎出来叫:
let fn = user.say;
fn();
这时候 fn 就是个普通函数了,this 就指向了全局(浏览器里就是 window),全局里没有 name,所以打印出 undefined。
在闭包里用 this 就更容易晕了。比如:
let fn = user.say();
这里返回的小函数被单独调用,this 又跑到全局去了,取不到 name。怎么解决呢?老办法是在外面用个变量把 this 存起来,比如 let self = this;,里面用 self。但现在更流行用“箭头函数”,箭头函数没有自己的 this,它会直接继承外面的 this:
return () => {
这样就能乖乖打印出“小明”了。不过箭头函数不能当构造函数用,也没有自己的 arguments 参数对象,所以用不用它,得看你的具体需求。
最后,咱们来聊聊函数里的“条件判断”。我给大家看一段真实的代码,这是一个获取音频的函数:
function getAudio(text, locale, voice, speed, volume, pitch) {
let spd = Math.round(speed * 15 / 100)
let vol = Math.round(volume * 15 / 100)
let pit = Math.round(pitch * 15 / 100)
let per = voice
let encText = encodeURIComponent(text)
let reqUrl = 'http://tsn.baidu.com/text2audio?tex=' + encText +
'&spd=' + spd + '&vol=' + vol + '&pit=' + pit +
'&per=' + per +
'&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=220&aue=1&res_tag=audio'
let resp = ttsrv.httpGet(reqUrl, {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
})
let code = resp.code()
let bodyBytes = resp.body().bytes()
let bodyStr = ttsrv.bytesToStr(bodyBytes)
if (code !== 200) {
let snippet = bodyStr.length > 300 ? bodyStr.substring(0, 300) + '...' : bodyStr
throw new Error('❌ 百度TTS状态码错误 (HTTP ' + code + '): ' + snippet)
let contentType = resp.header('Content-Type') || ''
if (contentType.length > 0 &&
contentType.indexOf('audio') === -1 &&
contentType.indexOf('octet-stream') === -1) {
throw new Error('❌ 百度TTS返回非音频格式 (Content-Type: ' + contentType + '): ' + snippet)
if (bodyBytes.length > 0 && (bodyBytes[0] === 0x7B || bodyBytes[0] === 0x3C)) {
let snippet = bodyStr.length > 200 ? bodyStr.substring(0, 200) + '...' : bodyStr
throw new Error('❌ 百度TTS返回文本错误 (疑似JSON/HTML): ' + snippet)
return new java.io.ByteArrayInputStream(bodyBytes)
你看这个 getAudio,它是个非常典型的“先请求,再查验,最后交货”的流程。拿到数据后,咱们没急着用,而是像小区保安一样,连续查了三道关:HTTP 状态码、响应头格式、内容特征。只要有一关没过,就直接扔出一个报错。
这里有个细节特别值得学:报错的时候,咱们不是干巴巴地喊一句“状态码错了”,而是把返回的内容截取前 300 个字符拼进去。为啥?因为光说错了,你根本不知道是参数传错了还是被限流了。但如果你能看到百度返回的具体提示,哪怕只有几百个字,排查问题也能快得多。这就是条件判断里的“诊断价值”。
第二道关查的是 Content-Type。正常成功的话,里面会带着 audio 或者 octet-stream。咱们用 indexOf 去查它包不包含这些词,而不是用 === 去死等。因为服务器返回的格式可能带点尾巴,比如 audio/mp3; codecs=...,用包含关系去查更稳妥,不容易漏网。
第三道关查的是“魔数”。JSON 数据开头通常是 {(十六进制是 0x7B),HTML 开头通常是 <(0x3C)。咱们直接看字节数组的第一个字节,如果是这俩,说明返回的根本不是音频,而是报错文本。直接看字节比转成字符串再看要快得多,也安全得多。
这种写法在编程里叫“守卫子句”。就像保安先查身份证、再查门禁卡,把有问题的全挡在门外,剩下的就是正常流程了。这样最后返回音频的代码就不用嵌套在层层 if 里面,代码看着特别清爽。在真实项目里,强烈建议大家多用这种“先排除异常,再走主流程”的写法。
好啦,条件判断咱们就结合这个例子讲透啦。至于什么三元表达式啊、更复杂的箭头函数高级用法啊,咱们今天就不死磕了。为啥呢?一是确实有点绕,小白容易听晕;二是写起来容易出错。咱们这套课主打一个“速成和实用”,不给大家增加不必要的心理负担。
从下节课开始,咱们就要真正去发 HTTP 网络请求啦!你已经学会了怎么赋值、怎么调用函数、怎么用条件判断检查数据合不合法,基础已经打得很牢了。
今天的课就到这里,大家下课!
目录
大家好!欢迎来到咱们 JS 脚本语言的第五堂课。
上节课呢,咱们算是跟“函数”这个新朋友打了个招呼,知道了怎么调用它,怎么给它传参数,还有它最后能吐出什么返回值。那今天咱们就要往深了挖一挖,讲讲返回值的一些“隐藏玩法”,以及 JS 里特别有名的一个概念——闭包。
先说返回值。上节课咱们讲了,函数能返回字符串、数字、数组、对象这些。但你们知道吗?它其实还能“返回另一个函数”!这就好比你去餐厅点了一份“神秘套餐”,服务员端给你的不是菜,而是一张“兑换券”,你得拿着这张券再去换真正的菜。
咱们来看个最简单的例子:
function greet() {
return function() {
console.log('Hello!');
};
}
let sayHello = greet();
sayHello();
你看啊,咱们调用 greet 的时候,它并没有直接打印 Hello,而是吐出来一个“还没被执行的函数”。咱们用 sayHello 这个变量把这个函数接住,然后再像普通函数那样去调用 sayHello(),这时候控制台才打印出 Hello。这就说明,函数当返回值是完全没毛病的,用起来也跟普通函数一模一样。
这时候你可能要问了:“干嘛这么麻烦?直接写个普通函数不行吗?”这就引出了咱们今天的一个重头戏——“闭包”。
闭包听起来挺高大上,其实用大白话讲,就是“函数记住了自己出生的老家”。当一个函数被返回到外面的时候,它不仅仅是把代码带出来了,它还把老家(也就是它定义时所在的环境)里的变量也一起打包带走了。哪怕外面的大函数已经执行完、下班了,这些变量也不会被销毁,而是被这个带出来的内部函数继续攥在手里。
咱们看个计数器的例子就明白了:
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
let counter = createCounter();
counter();
counter();
counter();
按理说,createCounter 执行完后,里面的 count 变量就该被系统回收了。但是!因为我们返回的那个小函数还在用着 count,所以系统就不敢回收它。于是每次调用 counter(),它都能在上一次的基础上接着加 1。这就是闭包最核心的本事:内部函数能一直抓着外部函数的变量不放,让变量的寿命变长了。
那闭包除了能当计数器,还有啥用呢?它最牛的地方在于能搞“私有化”。啥叫私有化呢?就像你把自己的日记本锁在抽屉里,别人想看你得通过你给的钥匙,不能直接翻。
咱们看这段代码:
function createPerson(name) {
let age = 0;
return {
setName: function(newName) {
name = newName;
},
getName: function() {
return name;
},
growUp: function() {
age++;
},
getAge: function() {
return age;
}
};
}
let person = createPerson('小明');
console.log(person.getName());
person.setName('小红');
console.log(person.getName());
person.growUp();
person.growUp();
console.log(person.getAge());
在这个例子里,name 和 age 就是被藏起来的“私有变量”。你没办法直接写个 person.age = 100 去改年龄,只能通过我们给的 growUp 方法让它一岁一岁长。这就像给数据装了一道防盗门,所有操作都得走正规渠道,程序自然就安全、可控多了。
这里给大家提个醒,闭包记住的是那个变量“本身”,而不是它当时的“照片”。如果外面有好几个内部函数,它们共享的是同一个变量。比如:
function createMulti() {
let x = 0;
return {
add: function() {
x++;
},
show: function() {
return x;
}
};
}
let obj = createMulti();
obj.add();
obj.add();
obj.show();
你看,add 和 show 盯上的都是同一个 x,add 把 x 加了两次,show 去读的时候,看到的就是 2。
那怎么判断代码里有没有闭包呢?记住一个死理儿:如果一个函数用了它外部的变量,并且这个函数被扔到外面去执行了,那闭包就产生了。最常见的套路就是“函数里返回函数”。
不过闭包有个小毛病,就是容易“占着茅坑不拉屎”。因为变量一直被引用,系统就不会回收它。如果你搞了太多闭包又不注意清理,内存就会被撑爆,也就是所谓的“内存泄漏”。所以不用闭包的时候,记得把外面的变量赋个 null,让内部函数找不到它,系统就能把内存回收了。
好啦,闭包咱们就聊到这。接下来咱们看看 JS 里的几个“关键字”。这些关键字是 JS 引擎自带的,用好了能事半功倍。
先说 function。咱们之前用它来声明函数,但它还能用来搞“函数表达式”,也就是直接把一个匿名函数塞给一个变量:
let sayHi = function() {
console.log('Hi!');
};
这里要注意个坑:用 function 直接声明的函数,系统会“提前打招呼”(也就是变量提升),你在代码前面调用它都没事;但如果是上面这种赋值给变量的写法,系统是不会提前打招呼的。你要是写在定义前面去调用,就会报错,因为那时候 sayHi 还是个 undefined(空壳子)。
再说 return。它是用来告诉函数“到此为止,带着东西下班”的。一旦碰到 return,后面的代码就不执行了。这里有个超级容易踩的坑:return 后面千万别直接换行!因为 JS 有个自作聪明的机制,会在换行的地方自动加分号。比如:
function foo() {
return
1;
}
你以为它返回 1,其实它返回的是 undefined。所以如果你想返回一个对象或者一长串东西,记得把大括号或者内容跟 return 写在同一行。
最后说说 this。this 是个“变色龙”,它的值取决于函数是怎么被叫出来的,而不是它写在哪。比如:
let user = {
name: '小明',
say: function() {
console.log(this.name);
}
};
user.say();
这里 say 是作为 user 的小弟(方法)被叫出来的,所以 this 指向 user,能打印出“小明”。但如果你把它单独拎出来叫:
let fn = user.say;
fn();
这时候 fn 就是个普通函数了,this 就指向了全局(浏览器里就是 window),全局里没有 name,所以打印出 undefined。
在闭包里用 this 就更容易晕了。比如:
let user = {
name: '小明',
say: function() {
return function() {
console.log(this.name);
};
}
};
let fn = user.say();
fn();
这里返回的小函数被单独调用,this 又跑到全局去了,取不到 name。怎么解决呢?老办法是在外面用个变量把 this 存起来,比如 let self = this;,里面用 self。但现在更流行用“箭头函数”,箭头函数没有自己的 this,它会直接继承外面的 this:
let user = {
name: '小明',
say: function() {
return () => {
console.log(this.name);
};
};
};
let fn = user.say();
fn();
这样就能乖乖打印出“小明”了。不过箭头函数不能当构造函数用,也没有自己的 arguments 参数对象,所以用不用它,得看你的具体需求。
最后,咱们来聊聊函数里的“条件判断”。我给大家看一段真实的代码,这是一个获取音频的函数:
function getAudio(text, locale, voice, speed, volume, pitch) {
let spd = Math.round(speed * 15 / 100)
let vol = Math.round(volume * 15 / 100)
let pit = Math.round(pitch * 15 / 100)
let per = voice
let encText = encodeURIComponent(text)
let reqUrl = 'http://tsn.baidu.com/text2audio?tex=' + encText +
'&spd=' + spd + '&vol=' + vol + '&pit=' + pit +
'&per=' + per +
'&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=220&aue=1&res_tag=audio'
let resp = ttsrv.httpGet(reqUrl, {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
})
let code = resp.code()
let bodyBytes = resp.body().bytes()
let bodyStr = ttsrv.bytesToStr(bodyBytes)
if (code !== 200) {
let snippet = bodyStr.length > 300 ? bodyStr.substring(0, 300) + '...' : bodyStr
throw new Error('❌ 百度TTS状态码错误 (HTTP ' + code + '): ' + snippet)
}
let contentType = resp.header('Content-Type') || ''
if (contentType.length > 0 &&
contentType.indexOf('audio') === -1 &&
contentType.indexOf('octet-stream') === -1) {
let snippet = bodyStr.length > 300 ? bodyStr.substring(0, 300) + '...' : bodyStr
throw new Error('❌ 百度TTS返回非音频格式 (Content-Type: ' + contentType + '): ' + snippet)
}
if (bodyBytes.length > 0 && (bodyBytes[0] === 0x7B || bodyBytes[0] === 0x3C)) {
let snippet = bodyStr.length > 200 ? bodyStr.substring(0, 200) + '...' : bodyStr
throw new Error('❌ 百度TTS返回文本错误 (疑似JSON/HTML): ' + snippet)
}
return new java.io.ByteArrayInputStream(bodyBytes)
}
你看这个 getAudio,它是个非常典型的“先请求,再查验,最后交货”的流程。拿到数据后,咱们没急着用,而是像小区保安一样,连续查了三道关:HTTP 状态码、响应头格式、内容特征。只要有一关没过,就直接扔出一个报错。
这里有个细节特别值得学:报错的时候,咱们不是干巴巴地喊一句“状态码错了”,而是把返回的内容截取前 300 个字符拼进去。为啥?因为光说错了,你根本不知道是参数传错了还是被限流了。但如果你能看到百度返回的具体提示,哪怕只有几百个字,排查问题也能快得多。这就是条件判断里的“诊断价值”。
第二道关查的是 Content-Type。正常成功的话,里面会带着 audio 或者 octet-stream。咱们用 indexOf 去查它包不包含这些词,而不是用 === 去死等。因为服务器返回的格式可能带点尾巴,比如 audio/mp3; codecs=...,用包含关系去查更稳妥,不容易漏网。
第三道关查的是“魔数”。JSON 数据开头通常是 {(十六进制是 0x7B),HTML 开头通常是 <(0x3C)。咱们直接看字节数组的第一个字节,如果是这俩,说明返回的根本不是音频,而是报错文本。直接看字节比转成字符串再看要快得多,也安全得多。
这种写法在编程里叫“守卫子句”。就像保安先查身份证、再查门禁卡,把有问题的全挡在门外,剩下的就是正常流程了。这样最后返回音频的代码就不用嵌套在层层 if 里面,代码看着特别清爽。在真实项目里,强烈建议大家多用这种“先排除异常,再走主流程”的写法。
好啦,条件判断咱们就结合这个例子讲透啦。至于什么三元表达式啊、更复杂的箭头函数高级用法啊,咱们今天就不死磕了。为啥呢?一是确实有点绕,小白容易听晕;二是写起来容易出错。咱们这套课主打一个“速成和实用”,不给大家增加不必要的心理负担。
从下节课开始,咱们就要真正去发 HTTP 网络请求啦!你已经学会了怎么赋值、怎么调用函数、怎么用条件判断检查数据合不合法,基础已经打得很牢了。
今天的课就到这里,大家下课!