JS 脚本语言第五课,函数的其他特性

大家好!欢迎来到咱们 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 网络请求啦!你已经学会了怎么赋值、怎么调用函数、怎么用条件判断检查数据合不合法,基础已经打得很牢了。


今天的课就到这里,大家下课!

发表于:2026-09-02 13:33
0个回复
  • 消灭零回复
您还没有登录,登录后才可回复。 登录 注册