JavaScript概述

前端时间阅读mdn,突然发现读下去就一发不可收拾,内容越看越多,越发觉得这门语言离我很远,归根结底,在于以前掌握的那些知识碎片化太厉害,以至于抓住JavaScript概念的一头往外牵拉,梳理不出明确的脉络结构,所以趁有些空闲时间,通过阅读mdn梳理下JavaScript的知识体系,把根扎牢,再读一些比较精深的文章,开枝散叶,重新认识这门编程语言。

什么是JavaScript?

当我们在问一个东西是什么的时候,一定是在问他的本质,即此物之所以为此物而非其他物的本质特性,mdn中解释得很清楚了:

JavaScript是基于原型编程、多范式的动态脚本语言。

"基于原型编程"即是说,JavaScript中的继承关系是通过原型链实现的,对象可以通过原型链继承另一个对象的属性和方法,而不是通过显式的构造函数。

"多范式"即是说,JavaScript可以支持多种不同的编程范式来编写程序,比如面向对象编程、命令式编程、声明式编程、函数式编程。

"动态"是说,JavaScript的变量是可以在运行时动态改变的,因而再JavaScript中,变量没有固定类型,这种动态类型特性使得JavaScript在处理数据时更加灵活和高效。

"脚本语言",JavaScript是在解释器中逐行解释执行,也可以在命令行中执行,所以是脚本语言。

JavaScript大致可以分成三部分:ECMAScript、文档对象模型(DOM)和浏览器对象模型(BOM)

而主要需要学习、理解和消化的部分就是ECMAScript,至于说DOM和BOM,其内容基本靠记忆,知道怎么用即可,我觉得过度研究是没有必要的。

当然,孰轻孰重,但凭个人决定。

JavaScript的优缺点

JavaScript最大的特点就是风格广泛,个人理解这个特点和JavaScript匆匆上线以及JavaScript创作者的知识广度分不开。

JavaScript部分语法来源于java,函数来自于scheme,原型继承来自于selef。

其最大的优点在于容易上手学习,因为JavaScript本质上很多复杂的概念都会用看起来很简单的方式体现出来,比如回调函数。

JavaScript开发者只是简单地使用这些特性,并不关心语言内部的实现原理。

对应地,JavaScript最大的缺点也暴露了出来------要真正掌握JavaScript是很难的,由于JavaScript本身很容易上手,大部分经过初步学习的开发者就可以编写功能全面的程序,而不需要像c++那样对程序有很深入的了解,因而很多人对JavaScript的学习态度就是维持现状,虽然能够胜任日常工作,但这样就很难真正掌握JavaScript。

这也是我目前存在的问题,知识碎片化格外严重,很多东西只是停留在会用、会写的阶段。

因此,在学习JavaScript的过程中,不仅只满足于代码正常工作,还要弄清楚"为什么"的根源问题。

要知其然,也要知其所以然,更要有侧重点和针对性,做到有轻有重,将JavaScript整体的知识结构化出来。

应该用怎样结构化的逻辑脉络,去覆盖JavaScript呢?

单纯将JavaScript分成ECMAScript、DOM和BOM没有丝毫意义,因为ECMAScript中的知识体量非常多,糅杂在一块儿就会变成一团浆糊。

最好的脉络,应该从JavaScript的特性出发,前面说过JavaScript是解释型语言,解释型语言就是通过解释器,当代码执行时逐条翻译成机器码。

但更准确的说法,它是即时编译(JIM)的语言,它在执行前会被即时编译成机器码,而不像传统编译语言那样预先编译成机器码,JavaScript的编译和运行是连续进行的,JavaScript在执行前一定会先经过编译,而编译后也会立即进入执行。

这种即时编译使JavaScript可以在运行时动态优化和改进代码的性能(比如热点代码更新),所以去学JavaScript,不仅仅学它的运行时,还要理解它的编译阶段,这是非常重要的,是串联后面很多知识点的关键。

所以学习JavaScript最好的脉络结构是:编译时+运行时

编译时

在传统编译语言中,程序中的源代码要经历三个阶段:词法分析、语法分析、生成代码和执行。

下面简述这三个阶段:

词法分析

这个过程会从左往右扫描JavaScript源代码,拆解成字符一个一个地读入,根据构词规则生成有意义的词法单元,即token。

例如:

css 复制代码
var a = 2

这段程序会分解成为下面这些词法单元:

css 复制代码
var
a
=
2

语法分析

语法分析会识别词法单元组成的各类语句或表达式,并转换成一个由元素逐级嵌套所组成的代表程序语法结构的树,即抽象语法树(AST)。

代码生成

将AST转换为可执行代码的过程称被称为代码生成,简单来说就是有某种方法可以将var a = 2;的AST转化为一组机器指令。

执行机器指令,就进入运行时阶段,比如上述代码中,创建一个叫作a的变量(包括分配内存等),并将一个值储存在a中。

因而我们在学习JavaScript也会大致分成三个部分:词法分析、语法分析、代码生成和运行时。

词法

在编程语言中,词法规定了语言的最小语义单元,即token,词法可以大致分为空白字符、换行符、注释和词,而其中词又分为标识符(变量名和关键字)、符号(运算符等)、数字直接量、字符串直接量和字符串模板。

编程语言的词法结构就是一套规则,这个规则用来描述如何使用这门语言来编写程序,它规定了变量名是什么样的,如何书写注释,语句之间如何分隔等。

字符集

JavaScript程序是用Unicode字符集编写的,而Unicode支持地球上几乎所有在用的语言。

JavaScript中的字符是区分大小写的,并且会忽略程序中token之间的空格,很多情况下会忽略换行符,由于可以在代码中使用空格和换行,因此可以采用整齐统一的编码风格,提高代码可读性。

注释

这个就不用说了,单行注释多行注释。

直接量

也叫字面量,就是程序中直接使用的数据值,比如12、"abc"、true、null等等。

标识符

包括变量名和保留字。

语法和语义

语法和语义部分内容可能会比较多,语义告诉你它是什么,语法告诉你它怎么用,语法语义大体上简要涵盖了JavaScript的知识点,这个阶段只要知道是什么,怎么用即可。

先说语法,JavaScript语法是由一系列的规则和约定组成的,用于定义代码的结构,比如变量声明、函数定义、条件语句等等,这些规则结构构成了JavaScript的语法。

语义指的是代码的含义,它描述的是代码中的元素(变量、函数、控制流语句等)的含义和作用。

在JavaScript中,词法和语法都是描述语言中的规则和结构,但概念和用途不同。

词法是描述程序语言的字符和符号,比如各种标识符、关键字、操作符、分隔符等,用于确定程序中个个字符和符号的语义和作用,决定了程序中的语法结构。

而语法这是用来描述语言的语义规则,包括程序的各个组成部分,比如变量、函数、语句、表达式等,用于确定程序中各个部分之间关系的规则,决定了程序的结构和行为。

这就类比于汉字,"猫"、"坐"、"在"、"草"、"垫"、"上",这六个字通过偏旁部首组合成汉字,我们知道它们都是合法的汉字,但要把组合成一句话,就要通过语法的规则去组合,"草"不能坐在"猫"身上,只有"猫坐在草垫上"是合法的句子,这就是语法,而语义则表示这个句子本身的含义,跟语法结构无关,即猫坐在草垫上这个现象,可以用中文表示,又可以用英语或日语表示。

JavaScript的语法主要包括:

  1. 变量声明:使用关键字var,let或const声明变量,其语义就是为其分配一个名称和数据类型。

  2. 表达式:使用运算符和操作数来执行计算,并返回一个值。

  3. 控制流语句:可以控制程序流程的语句,如条件语句(if、switch、while等)、循环语句(for、for-in、for-of等)、跳转语句(break、continue、return等)

  4. 函数定义:使用关键字function来定义函数,为其分配一个名称和参数列表,函数可以返回一个值或执行一些操作。

  5. 对象和数组:使用对象和数组来存储和处理数据,对象是由键值对组成的集合,而数组则是由一组有序的元素。

  6. 字符串和正则表达式:字符串和正则表达式来处理文本数据,字符串是字符序列,正则表达式则用于匹配和替换文本模式。

  7. 模块化:使用模块化编程组织和管理代码。

  8. 事件处理程序,使用事件处理程序来响应用户输入或其他事件,比如点击按钮等。

等等,只要在JavaScript写出来的代码,都可以归为语法和语义。

标识符

标识符就是用来标识具体对象的名称,最常见的标识符就是变量名,保留字也属于标识符。

值、变量和类型

值

值是存储在变量中的数据,可以是任何类型的数据,比如数字、字符串、null等,值是变量所代表的数据内容,通过变量名进行引用,值即字面量。

变量

变量是用来存储值的容器,它是特殊的标识符,用于标识一个值,变量可以通过赋值来存储值,也可以通过变量名来访问和操作值。

在JavaScript中,通过var、let、const和function声明变量,在引擎编译JavaScript程序时会获取所有被var和function声明的变量,会提升到代码的头部,即变量提升(预编译阶段)。

类型

类型是值的属性,定义了值的范围和用途,JavaScript中的类型有8种,包括七种原始数据类型(数字、字符串、布尔值、null、undefined、symbol和bigint)和一种引用数据类型(object,当然,还有array、set、map,但在这里都归为引用数据类型)

原始数据类型的值是存储在栈中的,而引用数据类型则是存储在堆中的,并在栈中存储指针用于指向这个数据。

这就表示栈最多只能有一个引用,堆可以有多个引用。

多数情况下,基本类型直接代表了最底层的语言实现,所有基本类型的值都是不可改变的,但需要注意的是,基本类型本身和一个赋值为基本类型的变量的区别:变量会被赋予一个新值,而基本类型不能像数组、对象以及函数那样被改变。

那么既然如此,基本类型应该是没有方法的,但它们仍然表现得像有方法一样,实际上这是包装对象在起作用,具体后面会讲。

这里大概了解下,当访问基本值的方法或属性时,JavaScript会自动将值装入包装器对象中,并访问该对象上的方法或属性。

例如:

arduino 复制代码
"foo".includes("f")

这里隐式创建了一个String包装对象,并在该对象上调用String.prototype.includes(),这种自动装箱行为在JavaScript代码中是无法观察到的,但却能很好理解为什么"改变"基本类型不起作用(因为str.Foo = 1不是赋值给str本身的Foo属性,而是赋值给了一个临时包装器对象)。

对引用数据类型来说,不应分为数组、对象、函数等,实质上引用数据类型只有对象。

运算符

运算符按照特定的运算规则对操作数进行运算,得出新的值。

算术运算符

除了对数字进行算术运算外,算术运算符会将非数字隐式转换为数字,比如+"5"的结果就是5。

并且还可以用作字符串的拼接。

关系运算符

根据关系的实际情况返回true或false。

比较大小的关系运算符(>、<、>=、<=)一般会进行数字转化并比较,除去比较大小关系的关系运算符外,还有如下常用的关系运算符:

in 左侧为字符串或可以转化为字符串的表达式,右侧为对象,如果对象拥有左侧名称一样的属性名,则返回true。

=== ==判断值是否全等和相等。

instance 左侧为对象,右侧为类,如果该对象是该类的实例,则返回true,计算o instance of f,JavaScript首先计算f.prototype,然后在原型链中查找o,如果找到,那么o就是f的一个实例。

逻辑运算符

逻辑运算符&&、||、!

赋值运算符

=、+=、-=、*=、/=、%=等等

其他运算符

delete 用于删除对象属性或数组元素,使用delete删除数组元素需要慎重,因为它不会改变数组长度,而会在删除元素的位置补上undefined。

new 创建新对象。

typeof 判断类型,基本可以判断所有基本类型,但对于引用数据类型,function是个例外。

对象

对于前面那些数据类型,大多都心里有个低,也不用拿来单独列几个章节了,当去繁从简,梳清脉络为主。

但对象却是不得不提的,它对于JavaScript非常之重要,以至于不得不在这里将它单独提炼成章节来做介绍。

创建对象

创建一个对象的方法常用的有三种:字面量、Object.create和new。

对象属性及属性的操作

在谈到对象属性时,往往会误认为这些属性值存储在对象内部,但这只是表现形式,在引擎中,属性值并不会存在对象容器内部,存储在对象容器内部的是这些属性的名称,类似指针一样指向这些值真正的存储位置,从本质上来说,属性就是引用。

对于属性操作,暂时记住这些方法或运算符即可:

  1. 删除属性:delete
  2. 检测属性:in、hasOwnProperty,前者会迭代查找原型链上的属性,后者只找自身属性。
  3. 枚举属性:for/in,思考为什么for/of不能用于对象?
  4. 存取器属性:ES5中,属性值可以用一个或两个方法替代,这个两个方法即getter和setter,由getter和setter定义的属性称为存取器属性。getter和setter是隐藏函数,分别在获取属性值设置属性值时调用。
  5. 属性特性:属性的特性有4个,即value(数据属性,默认为undefined)、writable(可写性,默认为true)、enumerable(可枚举型,能否通过for/in遍历)、configurable(可配置性,是否能修改属性特性)。存取器属性不具有值特性和可写性,它们的可写性是由setter方法存在与否决定的,因此存取器4个特性是读取get、写入set、可枚举和可配置性。属性特性可通过Object.getOwnPropertyDescriptor来获取对象的某个属性的属性特性。

对象特性

对象有三个特性,原型、类和可扩展性。

原型

JavaScript中每个对象一般都和另一个对象相关联,这另一个对象就是原型,每个对象都从原型上继承属性。

字面量创建对象的方式,具有同一个原型Object.prototype,通过new构造函数的方式,原型即是构造函数的prototype,Object.create则使用第一个参数作为对象原型。

但不是所有对象都有原型对象,比如Object.prototype。

原型之中往往是通过链式连接,比如new Date()继承Date.prototype,而Date.prototype继承Object.prototype,这样形式组成的链就是原型链。

检测一个对象是否是另一个对象的原型:isPrototypeOf。

javascript 复制代码
var hello = {}var song = Object.create(hello)console.log(hello.isPrototypeOf(song))

prototype是对象的特殊内置属性,是对其它对象的引用,所有prototype根据原型链都会指向Object.prototype,因此其包含JavaScript中许多通用的功能。

因此,给一个对象设置属性,不仅仅是添加新属性或修改已有属性,比如给对象o的属性x赋值,若o中已经有x,则设置该x的值,若o中不存在属性x,则添加一个属性x并赋值,若o是继承的x属性,则会检查原型链并判断是否可以赋值操作,x若为只读则赋值不被允许,若允许赋值,赋值操作也只会在原始对象上创建属性并赋值,而不会修改原型链。当x是setter,那就会调用这个setter,而不会添加到o

上,也不会重新定义x。这个特性即屏蔽,因此在JavaScript中,只有查询属性才会体会到继承的存在,设置属性则和继承无关,这是JavaScript的重要特性,这个特性决定了对象的多样性(Date、Promiese),可以让程序员有选择地覆盖继承属性。

而对于引用类型数据的修改,则会修改到原型链上的属性。

类

这个描述对象特性的类和面向对象语言中的类要区分开,这个类实际上就是字符串,用以标识对象的类型信息,只有一种方法可以间接查询该值,默认的toString方法,即继承自Object.prototype,会返回如下格式的字符串:object Class。

因此获取对象的类,可以调用对象的toString方法,然后通过正则去获取。

javascript 复制代码
console.log(Object.prototype.toString.call(cut).replace(/^\[object (\S+)\]$/, '$1'))

但对象的toString很多都被重写了,因此需要间接调用:Object.prototype.toString.call(o)其结果往往是包装对象的名称,诸如Date、Regexp、Array、Window、String等等

可扩展性

对象的可扩展性表示是否可以给对象添加新属性,所有内置对象和自定义对象都是可扩展的。

可以通过Object.isExtensible方法判断对象是否可扩展。

不可扩展:如果要转化为不可扩展,需要调用Object.preventExtensions(),注意一旦将对象转换为不可扩展就无法再将其转换为可扩展了。

封闭:Object.seal()除了能把对象设置为不可扩展外,还会将所有属性设置为不可配置,即不能添加新属性,原有属性也不能删除或配置,不过对属性可写,通过Object.isSealed()检测对象是否封闭。

冻结:Object.freeze()严格锁定对象,不可扩展、不可配置、不可写,通过Object.isFrozen()来检测对象是否冻结。

序列化对象

对象的序列化指的是将对象的结构状态转换为字符串描述,在ES5中提供JSON.stringfy方法用于将对象序列化,JSON.parse还原Javascript对象。

JSON语法是JavaScript语法的子集,并不能表示JavaScript里的所有值,NaN、Infinity和-Infinity序列化结果为null、函数、RegExp和Error以及undefined无法序列化,会被忽略掉。

如何赋值一个对象,作为JavaScript初学者常见问题。

对于浅拷贝而言,复制的对象中,简单类型的属性值会直接复制,但引用类型的值只是复制引用,和原先对象中属性值引用的对象是一样的。

对深拷贝而言,复制对象属性时,还要复制引用和值。对于json安全的对象,即可以序列化为一个json字符串,并且可以根据这个字符串解析成一个结构和值完全一样的对象,有一种巧妙的复制方法:var newObj = JSON.parse( JSON.stringify( someObj ) );浅拷贝非常易懂且问题很少,ES6定义了Object.assign方法实现浅拷贝,参数一是目标对象,之后可以添加多个源对象。

它会遍历多个源对象的所有可枚举的自有键,并将其复制到目标对象,最后返回目标对象。

ini 复制代码
var newObj = Object.assign( {}, myObject );

类

和前面表示对象特性的类区分开,这里的类描述了一种代码的组织结构形式:一种反映真实世界的建模方法。

面向对象编程强调的是数据和操作数据的行为互相关联,好的设计就是把数据和它相关联的行为封装起来,即数据结构。

比如一串字符通常被称为字符串,字符就是数据,但往往关心的不是数据是什么,而是可以对数据做什么,所以可以应用在这种数据上的行为,比如计算长度、添加数据等等,都应该被设计成String类的方法。

在ES6以前JavaScript只有通过构造函数去模拟类,ES6新增class关键字,但实际上也只是类的语法糖,JavaScript从来没有出现类的概念,只是一种可以理解为"类"的设计模式。

JavaScript和面向类语言不同,它没有类,只有对象,JavaScript通过prototype的共有且不可枚举的特性来模仿类。

lua 复制代码
function Foo() {  // ... }var a = new Foo();console.log(Foo.prototype.isPrototypeOf(a)) //true

这段代码通过new 构造函数的方式实例化一个对象,new会生成一个新的空对象,并将内部的this指向这个对象,然后将对象的prototype关联到Foo的prototype上,最后return出来,这是new的执行流程。

继承

几种类继承的方式如下:

原型链继承

原型链继承即简单利用原型的关联进行继承:

scss 复制代码
function Parent(name){  this.name = name;  this.like = [1,2,3]}function Son(){};Son.prototype = new Parent('song');son1 = new Son();son2 = new Son();son2.like.push(99)console.log(son1.like)

当son2对属性进行修改时,所有实例的like属性都被修改了。

在原型链继承中,子类继承父类的方式是调用一次父类,实例化一个对象并关联子类的prototype,那么子类在实例化对象后,原型都会指向这个prototype。

虽然原型链继承可以完成继承关系,但由于父类只调用一次,因而Son.prototype中存在引用类型的数据时,当实例修改这个引用类型的数据,其他实例化对象也会受到影响。

且由于父类调用一次,子类在实例化时无法向父类传递参数。

借用构造函数继承

为了解决原型链继承的问题,借用构造函数继承会在子类的实例上创建父类的属性。

scss 复制代码
function Parent(){  this.like = [1,2,3]}function Son(){  Parent.call(this)};son1 = new Son();son2 = new Son();son2.like.push(99)console.log(son1.like)console.log(son2.like)

运行结果:

1, 2, 3

1, 2, 3, 99

由于每次实例化子类,都会执行一次父类,执行父类的结果是给每个实例对象上添加属性,也就是说这个属性不是原型上的属性,而是实例对象自身的属性,因此修改不会影响到其他实例,并且在子类实例化对象时还可以进行传参。

它解决了原型链继承的问题,即可以传递参数,修改属性不会影响到其他实例

但只能继承父类的实例属性和方法,而无法继承父类的原型上的属性和方法。并且无法复用,每个子类实例相当于"复制"了父类实例的函数,影响性能。

组合继承

组合继承就是综合原型继承和借用构造函数继承,即用原型链实现原型属性和方法的继承,借用构造函数继承实现实例属性的继承。

scss 复制代码
function Parent(){  this.like = [1,2,3]}Parent.prototype.Say = function(){  console.log(this.like)}function Son(){  Parent.call(this)};Son.prototype = new Parent()Son.prototype.constructor = Sonson1 = new Son();son2 = new Son();son2.like.push(99)son1.Say()son2.Say()

运行结果:

1, 2, 3

1, 2, 3, 99

组合继承父类执行了两次,第一次执行给子类的prototype写入属性like并关联父类的prototype,第二次执行是给实例对象创建属性like,并且在第一次执行时,由于子类prototype中的constructor变为父类,因此需要手动更换回来。

优点在于组合了原型继承和借用函数继承,中和了两者的缺点。

缺点在于:原型中复制了父类的实例,即这里的like属性在对象实例和prototype中各有一份。

寄生组合式继承

由于组合继承中的父类调用了两次,即在实例化调用一次,指定子类原型时调用一次,父类中的属性势必会在子类原型和实例化对象中各"复制"一份,因此寄生组合式继承就是为了解决这个问题。

matlab 复制代码
    function Parent() {      this.like = [1, 2, 3]    }    Parent.prototype.Say = function () {      console.log(this.like)    }    function Son() {      Parent.call(this)    };    // Son.prototype = new Parent()    Son.prototype = Object.create(Parent.prototype)    Son.prototype.constructor = Son    son1 = new Son();    son2 = new Son();    son2.like.push(99)    console.log(son1,son2)

运行结果:

1, 2, 3

1, 2, 3, 99

在指定Son.prototype的原型时不再调用父类,而是通过Object.create返回指定对象的原型。

这种继承方式基本解决了以上所有的问题,也被称为经典继承,但依旧存在问题,也就是子类的prototype被重写,因而prototype为空对象,因此实例化对象无法获取到原本子类的原型。

解决方案有两种:一是在重写子类prototype后,手动添加prototype的属性和方法,二是通过圣杯模式

class

s6出现class关键字,作为类的语法糖,通过extends关键字实现继承。

scala 复制代码
    class Parent{      constructor(name){        this.name = name      }    }    class Son extends Parent {      constructor(name,age){        super(name)        this.age = age      }    }    const song = new Son('song',18)

上述代码使用class关键字创建类,每个类包含特殊方法constructor,即构造函数,该函数用于创建和初始化由class创建的对象。通过extends来实现子类对父类的继承,super()方法用于调用父类的构造函数,这样就可以访问父类的属性和方法,当super为方法时即父类的构造方法,当super为对象时则为父类。

另外,在class中有个关键字static,即在类中定义一个静态方法,静态方法通过static关键字修饰,又叫类方法,属于类,不属于对象,通过类名直接访问。

class类的继承方式实际上就是模拟的寄生组合式继承,并解决了寄生组合式继承的缺点。

总结来说,类是一种设计模式。

许多语言提供了对于面向类软件设计的原生语法,JavaScript也有类似的语法,但是和其他语言中的类完全不同。

类意味着复制,传统的类被实例化时,它的行为会被复制到实例中,类被继承时,行为也会被复制到子类中。

JavaScript无法完全模拟类的复制行为,只能复制引用,而无法复制被引用的对象或者函数本身。

相关推荐
定栓7 分钟前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
小熊在忙fpga16 分钟前
N32G430芯片的flash表示
前端·javascript·html
Data analyse45622 分钟前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
NC 王炸28 分钟前
AI 做的便签,换个浏览器就没了?加一个导出按钮,把数据带走
javascript·ai编程·数据备份·localstorage·txt导出
todoitbo28 分钟前
本来只想给内网开个口子,Natapp 却多给了一道防护
前端·javascript·vue.js·内网穿透·网络防护·natapp
会灭火的程序员33 分钟前
SuperMap AgentX Server智能体接入WEB应用技术文档
前端·supermap·agentx·agentx server
CRMEB38 分钟前
商品 SKU 设计,从简单规格到多规格踩过什么
java·前端·数据库·开源
Chloe.Zz39 分钟前
一篇入门Agent
开发语言·前端·javascript
java1234_小锋1 小时前
【技术专题】HTML5 网页技术 - HTML5 表格标签
前端·html·html5
silvamux_1 小时前
4款新模型商务邮件翻译横评:0.98到0.46
android·java·javascript