当前位置: 首页 > news >正文

为什么你始终看不懂JavaScript?

为什么你始终看不懂JavaScript?

  • JavaScript的“两面性”陷阱
  • 语言设计哲学:妥协与进化
    • 诞生背景:10天创造的“应急语言”
      • 1. 向后兼容的代价
      • 2. 弱类型带来的灵活性和混乱
    • 双重身份:函数式与面向对象的混合体
      • 面向对象的方式
      • 函数式的方式
      • 函数式与面向对象混用
  • 编译-执行双阶段模型:理解诡异行为的关键
    • 编译阶段(预解析)
      • 1. 变量提升
      • 2. 函数提升
        • 函数提升的变种:函数表达式
      • 3. 作用域链建立
    • 执行阶段
    • 执行上下文与闭包的秘密
  • 解释型语言的动态特性
    • 运行时类型检查与转换
    • 原型链:JavaScript的继承机制
      • 原型链查找
  • 异步编程模型:事件循环
  • 实战:理解一道经典面试题
  • 结语

看似简单的语法背后,隐藏着令人费解的行为逻辑——这是无数前端开发者共同的困惑,本文将深入探讨 JavaScript 的两面性与其“诡异行为”。

JavaScript的“两面性”陷阱

作为一名前端工程师,我经常听到有人抱怨:“JavaScript 的语法明明很简单,为什么写起来总是踩坑?”这正是 JavaScript 最迷人的地方,也是它最令人困惑的地方——简单语法与复杂行为的强烈反差。

让我们先从一个经典的“诡异”例子开始:

console.log(1+"2");// "12" 还是 3?console.log(2-"1");// 1 还是 报错?console.log(true+false);// 1?还是true?console.log([]+[]);// ""?还是[]?console.log([]+{});// "[object Object]"?为什么?

这些看似简单的表达式,结果却常常出人意料。接下来,我们将从JavaScript的设计哲学入手,揭示这种“双重人格”背后的秘密。

语言设计哲学:妥协与进化

诞生背景:10天创造的“应急语言”

JavaScript 诞生于1995年,网景公司为了在浏览器中添加简单的交互功能,仅用10天就设计了这门语言。这种“速成”背景决定了它的一些特性:

1. 向后兼容的代价

typeofnull==="object"// 著名的设计错误,但已无法修改

2. 弱类型带来的灵活性和混乱

leta=10;// 现在是数字a="hello";// 突然变成字符串a=function(){return42;};// 又变成函数

双重身份:函数式与面向对象的混合体

在 JavaScript 中,它同时支持两种编程范式,这既是优势,也是产生困惑的源头:

面向对象的方式

classPerson{constructor(name){this.name=name;}greet(){return`Hello,${this.name}`;}}

函数式的方式

constcreatePerson=(name)=>({name,greet:()=>`Hello,${name}`});

函数式与面向对象混用

constperson={name:"zhangsan",greet(){return`Hello,${this.name}`;},// 函数式的方法toUpperCase:function(){returnthis.name.toUpperCase();}};

编译-执行双阶段模型:理解诡异行为的关键

这是本文的核心重点!JavaScript 的行为之所以令人困惑,很大程度上是因为它的双阶段执行模型。

编译阶段(预解析)

在这个阶段,JavaScript引擎会做三件重要的事情:

1. 变量提升

我们先来看一段简单的代码:

console.log(a);vara=10;console.log(b);letb=20;

上述代码在编辑阶段会发生什么呢?

vara;// var声明被提升,初始化为 undefined// let b; // let声明也被提升,但不会被初始化(暂时性死区)

在部分资料中提到 var 与 let/const 的区别,中间会有一点:let/const不会出现变量提升。这种说法是不准确的。其实 let/const 也会出现变量提升,只是在提升后并不会被初始化,在这个阶段,直接调用变量程序会报错,因此被称为:暂时性死区

2. 函数提升

sayHello();// 可以正常调用!functionsayHello(){console.log("Hello!");}

我们可以看到函数提升是可以正常调用的,这又是为什么呢?原来,在 JavaScript 中,函数提升,会把整个函数声明(包括函数体)都提升到顶部,其实际执行过程如下:

functionsayHello(){// 整个函数声明(包括函数体)提升到顶部console.log("Hello!");}sayHello();// "Hello!"
函数提升的变种:函数表达式

在函数定义时,我们也可以将函数复制给一个变量,即函数表达式,这种情况下,又会产生新的问题:

sayHello();// TypeError: foo is not a functionvarsayHello=function(){console.log("Hello!");}

可以看到这种情况下,又出现了新的问题。其本质仍然在于关键字 var,将整个函数作为了变量进行处理。

3. 作用域链建立

functionouter(){vara=10;functioninner(){// 编译时就知道可以访问aconsole.log(a);}returninner;}

关于作用域链,在后面的文章中会详细讲解!

执行阶段

执行阶段按顺序运行代码,但此时作用域、变量状态都已确定。还是看看变量提升的例子:

console.log(a);vara=5;console.log(a);

其执行过程是什么样的呢?

vara;// 编译阶段:声明提升,初始化为 undefinedconsole.log(a);// 执行阶段:输出 undefineda=5;// 执行阶段:赋值 5console.log(a);// 执行阶段:输出 5

执行上下文与闭包的秘密

理解执行上下文是掌握JavaScript的关键:

functioncreateCounter(){letcount=0;// 这个变量会被"闭包"捕获return{increment:function(){count++;returncount;},decrement:function(){count--;returncount;}};}constcounter=createCounter();console.log(counter.increment());// 1console.log(counter.increment());// 2// count变量"神奇地"被记住了,即使createCounter已经执行完毕

关于执行上下文与闭包的相关内容,在后面的文章中,会详细讲解!

解释型语言的动态特性

运行时类型检查与转换

JavaScript 的类型系统在运行时动态工作,这导致了许多“魔幻”般的行为。还记得我们文章开头的那个例子吗,其正确的输出结果是:

console.log(1+"2");// "12"console.log(2-"1");// 1console.log(true+false);// 1console.log([]+[]);// ""console.log([]+{});// "[object Object]"

这中间其实存在许多隐式转换规则:

console.log(0==false);// trueconsole.log(""==false);// trueconsole.log([]==false);// trueconsole.log(null==undefined);// trueconsole.log("0"==false);// true

因此,我们在实际开发中,推荐使用===进行判断,防止类型转换带来的问题。

console.log([] + {});为什么输出结果是[object Object]呢?这又涉及到 Object 对象的原型链方法。这段代码等价于:console.log([].toString() + ({}).toString());。其中:[]会被转成空串""{}会被当做一个对象,被转成[object Object]。(这是Object.prototype.toString()的默认实现)

原型链:JavaScript的继承机制

这是JavaScript最独特也最令人困惑的特性之一:

// 原型链示例functionAnimal(name){this.name=name;}Animal.prototype.speak=function(){console.log(`${this.name}makes a noise`);};functionDog(name){Animal.call(this,name);// 调用父类构造函数}// 设置原型链Dog.prototype=Object.create(Animal.prototype);Dog.prototype.constructor=Dog;Dog.prototype.speak=function(){console.log(`${this.name}barks`);};constdog=newDog("Rex");dog.speak();// "Rex barks"

原型链查找

上述过程存在一个原型链查找过程:

  1. dog.hasOwnProperty('name'):true,直接调用
  2. dog.hasOwnProperty('speak'):false,往原型上查找
  3. dog.__proto__.hasOwnProperty('speak'):true

关于原型和原型链的内容,在后面的文章中会详细讲解。

异步编程模型:事件循环

JavaScript 的单线程异步模型,这又是另一个难点了,我们先来看一道经典的面试题:

console.log("1");setTimeout(()=>{console.log("2");},0);Promise.resolve().then(()=>{console.log("3");});console.log("4");

上述代码的输出结果是:1 4 3 2

我们再看一道事件循环的微观队列与宏观队列的代码:

console.log("start");setTimeout(()=>{console.log("setTimeout");},0);Promise.resolve().then(()=>{console.log("P1");}).then(()=>{console.log("P2");});console.log("end");

上述代码的输出结果是:start end P1 P2 setTimeout

关于 JavaScript 异步编程的相关内容,在我的另外一个专栏里: Promise详解
有详细介绍!

实战:理解一道经典面试题

让我们用今天学到的知识解析一道经典面试题:

for(vari=0;i<5;i++){setTimeout(function(){console.log(i);},100);}

上述代码的输出结果是:5 5 5 5 5。为什么不是0 1 2 3 4呢?

  1. var声明的变量i是函数作用域(或全局作用域)
  2. 所有setTimeout共享同一个i
  3. setTimeout回调执行时,循环已结束,i=5,所以输出都是 5 。

这种问题应该如何解决呢?

  1. 使用let(块级作用域):
for(leti=0;i<5;i++){setTimeout(function(){console.log(i);// 0,1,2,3,4},100);}
  1. 使用闭包:
for(vari=0;i<5;i++){(function(j){setTimeout(function(){console.log(j);// 0,1,2,3,4},100);})(i);}

结语

JavaScript的“诡异”行为并非缺陷,而是其灵活性和强大功能的副产品。理解它的双阶段模型、作用域链、原型系统和事件循环,是掌握这门语言的关键。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

http://www.jsqmd.com/news/459461/

相关文章:

  • vue3:最新实现腾讯人脸核身+增强版人脸核身使用方法及示例源码,Vue3如何使用腾讯云慧眼人脸核身,提供人脸核身案例、身份信息核验、活体检测与核身比对等示例代码(后端spring与thinkphp)
  • Cursor 生死存亡:AI 编程痛点解决效率提升实战
  • LiuJuan Z-Image Generator实战案例:跨境电商独立站产品模特图生成SOP
  • Kdevelop使用步骤详解(Linux)
  • vue3:解决天地图api调用报403(Forbidden)错误,调用天地图服务出现403错误(或者部署到服务器后天地图API接口返回403详细排查和解决方法)彻底解决天地图403 Forbidden
  • Qwen3-0.6B-FP8详细步骤:Web界面主题定制+Logo替换与品牌化部署
  • 2026年知名的锦纶面料品牌推荐:coolmax面料/阳离子面料/石墨烯面料厂家最新推荐 - 行业平台推荐
  • 百川2-13B-4bits开源镜像部署案例:教育机构用作AI助教,支持学生编程答疑
  • (第二篇)Spring AI 实战进阶:从 0 搭建 SaaS 模式多租户 AI 客服平台(核心难点 + 性能优化全解析)
  • 部署完OpenClaw不能随时用!郁闷不?cpolar能解锁N种随身玩法!
  • [React Native for OpenHarmony] 将代码提交至AtomGit平台自建公开仓库用于分布式管理
  • PETRV2-BEV训练效果惊艳:BEV检测结果叠加原始环视图像的精准对齐展示
  • 【FacePoison+】Hiding Faces in Plain Sight: Defending DeepFakes by Disrupting Face Detection
  • Bidili Generator保姆级教程:解决SDXL显存高、LoRA不兼容痛点
  • ROS2(服务,launch,工具,仿真)
  • Comsol 探索光子晶体谷霍尔效应:从单胞、超胞到谷单向传输
  • GPEN推荐配置标准:8GB以上显存实现流畅批量处理
  • 重邮虚拟机环境配置(二)如何在本机和虚拟机之间的文件传输
  • Gemma-3-12B-IT入门指南:首次加载模型耗时优化与缓存机制说明
  • Gemma-3-12B-IT WebUI部署教程(含Supervisord管理):生产环境就绪
  • Qwen-Image-Edit-2509与SDXL对比:图像编辑灵活性实战评测
  • 重邮虚拟机环境配置(三)如何在虚拟机上安装驱动
  • Neeshck-Z-lmage_LYX_v2应用场景:短视频封面图批量生成+风格统一控制方案
  • Z-Image-GGUF内容创作:短视频脚本→分镜图→动态视频的AI协同路径
  • BUUCTF 后门查杀1
  • MYSQL个人笔记总结
  • Sonic数字人如何集成ComfyUI?可视化工作流导入步骤详解
  • 2026年比较好的深圳ETFE太阳能光伏板厂家推荐:深圳玻璃太阳能光伏板/深圳非标定制太阳能光伏板高评价厂家推荐 - 行业平台推荐
  • Wan2.2-T2V-A5B开发手册:自定义工作流创建与保存技巧
  • Gitea 版本控制服务端