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无法完全模拟类的复制行为,只能复制引用,而无法复制被引用的对象或者函数本身。

相关推荐
还是大剑师兰特28 分钟前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解29 分钟前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~35 分钟前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding40 分钟前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT44 分钟前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓44 分钟前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶213644 分钟前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了44 分钟前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django
张张打怪兽1 小时前
css-50 Projects in 50 Days(3)
前端·css