JavaScript基础知识体系

592 阅读38分钟

这一套笔记是我啃JavaScript高级程序设计时整理的,帮助我的理解和架构知识体系。找到实习后不希望之前的努力被尘封,于是拿出来希望给新入坑的小伙伴提供帮助。如果其中存在错误,也欢迎指正。

JavaScript简介

JavaScript组成

  • 核心(ECMAScript)

  • 文档对象模型(DOM)

  • 浏览器对象模型(BOM)

ECMAScript

ECMAScript只是定义了JavaScript语言的基础,大致为以下内容:

  • 语法

  • 类型

  • 语句

  • 关键字

  • 操作符

  • 对象

web浏览器只是其宿主环境之一,宿主环境不仅提供基本的ECMAScript实现,还提供了语言的相关拓展,如DOM等。宿主环境包括:web、node、Adobe Flash等。

文档对象模型(DOM)

  • DOM1级:映射文档结构

  • DOM2级:

    • DOM视图

    • DOM事件

    • DOM样式

    • DOM遍历和范围

  • DOM3级:

    • 统一加载和保存文档的方法(DOM加载和保存模块)

    • 验证文档的方法(DOM验证模块)

浏览器对象模型(BOM)

BOM实际上只包括处理浏览器窗口和框架;但人们习惯上吧所有针对浏览器的JavaScript拓展算作BOM的一部分。比如:

  • 弹出新浏览器窗口

  • 移动缩放和关闭浏览器窗口

  • 提供浏览器详细信息navigator对象

  • 提供浏览器所加载页面的详细信息的location对象

  • 提供用户显示器分辨率详细信息的screen对象

  • 对cookies的支持

  • 像XMLHttpRequest这样的自定义对象

在HTML中使用JavaScript

外源知识点:MIME类型

媒体类型MIME是一种标准,用于表示文档、文件或字节流的性质和格式。

通用格式:type/subtype(对大小写不敏感,通常是小写)

独立类型:

  • text:普通文本,人类可读

  • image:图片

  • audio:音频文件

  • video:视频文件

  • application:某种二进制数据

在HTML中使用JavaScript的方法:

  • 嵌入代码

  • 外部文件

外部文件

在<script>元素中有以下属性:

  • async:异步脚本;

  • charset:淘汰中;

  • defer:延迟脚本;

  • language:已淘汰

  • src:包含外部文件

  • type:指示脚本语言的内容类型

以上属性中,需要特别注意的就是async异步执行和defer延迟执行了。这两个本质上都属于异步的范畴:

  • async是异步下载,立即执行;

  • 而defer是异步下载,在解析完HTML页面后执行。


外源知识点:浏览器渲染过程

  • HTML解析为DOM Tree

    将标记转化为DOM树的形式

  • CSS解析为CSS Rules Tree

    多层选择器是从下向上逐个检索的,很耗效率,尽量避免多层的情况。

  • 构建渲染树

    通过综合上面两个树,形成了包括节点内容和节点样式的渲染树。需要注意的是渲染树并不是包括DOM中的所有节点,只包括需要展示的节点。

  • Layout根据渲染树计算每一个节点的信息

  • Painting根据计算好的信息来绘制

外源知识点:回流和重绘

  • 重绘:对某个区域、对象的重新渲染

  • 回流:对某个区域、对象重新渲染,并影响他的祖先一起重绘,可能会导致顶级祖先也就是整个文档重绘。

重绘的消耗很小,浏览器不需要为其重新计算样式信息,所以直接进入Painting阶段。

而回流的消耗就很大,因为样式或DOM的改变,渲染树需要重新生成,所以以上的步骤需要重新走一遍,自然耗费巨大。

回流必定发生重绘,重绘不一定发生回流

常见的引起回流的方法:

  • 添加或删除可见的DOM元素

  • 元素尺寸改变

  • 文本内容变化

  • 浏览器窗口尺寸改变

  • 增加或移除样式表

  • 激活CSS伪类

  • 操作Class属性

  • 计算offsetWidth和offsetHeight

  • 设置style的值

外源知识点:优化回流

浏览器本身的优化策略:浏览器维护一个队列,把要回流和重绘的任务放在这个队列,当队列中的任务数到达阈值或经过一定时间,浏览器就会将队列中的任务统一批处理执行。这样多次的回流的计算就合并成了一次计算。

存在一些代码强制浏览器立即执行回流操作。

优化回流就是充分利用浏览器的优化回流:

  • 将多次改变DOM的操作合并在一起

  • 将需要多次重拍的元素,position设为absolute或者fixed,这样此元素就脱离了文档流,他的元素重绘就不会回流向上影响祖先元素。例如有动画的元素就尽量设置为absolute

  • 在内存中操作节点,最后再加载入文档中

  • 由于display为none的元素不在渲染树中,利用这一项可以将复杂的元素操作安排在元素的隐藏期间完成,这样,只需两次渲染就可以完成操作。

  • 在经常获取会引发浏览器重新渲染的变量时,将其缓存。上面介绍过相关的操作

  • 避免使用table布局

  • 避免多项内联样式

外源知识点:DOM、Node、NodeList、HtmlCollection

DOM(文档对象模型)用逻辑树来表示一个文档,每个节点(node)都包含一个对象。

Node实际上是一个接口,很多的DOM API对象都会从这个接口中继承,比如:

Document, Element, Attr, CharacterData (which Text, Comment, and CDATASection inherit), ProcessingInstruction, DocumentFragment, DocumentType, Notation, Entity, EntityReference

NodeList是一个Node对象的集合,是一个类数组对象,但不是一个数组,forEach()来迭代;也可以使用Array.from()将其转化为数组。

来源:childNodes、querySelectorAll()的返回值。

  • 动态集合:childNodes返回的对象为动态的,对DOM结构的变化能够自动反映到所保存到对象中。

  • 静态集合:querySelectorAll返回的对象为静态的,不随DOM改变。

HtmlCollection接口表示了一个元素(Element)(顺序为文档顺序)的集合。Element只是12种Node中的一种。

来源:getElementsByTagName()getElemetsByClassName()getElementsByName()等方法的返回值,以及children、document.links、document.forms等元素集合。

所有的HtmlCollection对象都是动态的。

区别:NodeList与HtmlCollection存在很大不同,区别主要来自Node与Element的差异。在HTML文本中的换行与空格等也算作是Node(Text),而不是Element。

基本概念

语法、数据类型、流控制语句、函数

语法

标识符惯例:

小驼峰式:theNameTag

注释

 //单行注释 /* *这是 *多行 *注释 */

严格模式

  • 全局模式:在顶部添加:"use strict"

  • 局部模式:在函数顶部添加:"use strict"

变量

外源知识点:var、let、const

声明方式变量提升暂时性死区重复声明初始值作用域
var允许不存在允许不需要除块级
let不允许存在不允许不需要块级
const不允许存在不允许需要块级
变量提升

变量可以在声明前使用,ES6规定let和const不发生变量提升。

暂时性死区

与变量提升相关,因为let和const不存在变量提升,所以在一个块代码中,声明变量之前部分的区域就是死区。

重复声明

……

初始值

因为const不能赋值,所以必须有初值。

作用域

var的除块级是说:var的作用域是在代码块之上函数体之下的。

 if(true){ var a = 1; } console.log(a)//1

另外,变量的声明还有一种方法:

直接声明:name = '';,这样声明的变量就是全局变量,无论声明的位置在哪……

外源知识点:全局变量的声明

  • 在外部声明变量

  • 不加var,作为非严格模式中的过错补救方式,直接将该变量作为全局变量。

  • 直接将该变量显式的添加到window(页面变量)中。

数据类型

typeof操作符

  • undefined:未定义,声明未赋初值的和未声明的变量均为undefined

  • boolen:……

  • string:……

  • object:表示对象或null

  • number:……

  • function:包括括号函数

null类型

null返回的是一个空指针,所以使用typeof检测null返回的是object。需要注意的是,null与undefined之间的==永远返回true(大概是因为他们都能被自动转换为false)

 null == undefined //true null === undefined //false

Boolean类型

虽然Boolean类型的字面值只有两个,但是ECMAScript中所有类型的值都有与这两个等价的值

数据类型转换为true的值转换为false的值
Booleantruefalse
String非空字符串“”(空字符串)
Number任何非零数字(包括无穷大)0和NaN
Object任何对象null
Undefinedn/a(not applicable 不适用)undefined

Number类型

十进制、八进制、十六进制声明方法:

 var intNum = 55; var octalNum = 055;//第一位为零,默认为八进制。若后续数字超出范围,忽略零。 var hexNum = 0x55;//前两位为0x

进行算数运算时,八进制和十六进制都会被转换为十进制进行计算。

MIN_VALUE:5e-324

MAX_VALUE:1.8e308

Infinity:无穷,当数值的绝对值超过范围时,会自动转换为Infinity或-Infinity

NaN:非数值,用于本应返回数值的操作数未返回数值的情况(避免报错)。需要注意的是,NaN不与任何数相等,包括它本身:NaN == NaN;//false

判断NaN的方法:isNaN()

数值转换(转型函数)

这里需要区分转型函数与同名的基本包装类型的区别,前者是一个函数,返回值是基本数据类型;而后者是基本包装类型(引用类型的一种),创建的是引用类型的一个实例(object)

Number()

  • Number()可以转换所有类型的变量。非字符串参数根据转换规则转换,以下为转换字符串的规则

  • 存在非数字字符,转换为NaN

  • 空字符串转换为0

  • 十六进制转换为十进制,八进制直接忽略前导零。

  • 浮点数正常转换

parseInt()

  • parseInt()只能转换字符串,以下是其与Number()的区别

  • 忽略开头的空格,找到第一个非空格字符开始计算,若第一个非空格字符不是数字,返回NaN

  • 根据上面的规则,空字符串转换为NaN

  • 忽视数字后面跟着的非数字字符parseInt('123sff');//123

  • ECMAScript3识别八进制,但是ECMAScript5不再自动识别八进制。为了识别可以加上第二个参数(进制)

    parseInt('070',8);

parseFloat()

  • 只解析十进制

  • 条件许可时,会返回十进制。

####

String类型

单引号与双引号没有区别。

字符串变量不可变

当对一个字符串赋值时,首先创建一个新的字符串变量存储新的变量值,然后将原先的字符串变量销毁。

转换为字符串

toString():几乎所有的变量都有toString()方法,除了null和undefined。

String():当不确定变量是否为null或undefined时,可以使用String()方法转换。

Object类型

对象其实就是一组数据和功能的集合。

操作符

仅介绍几个需要注意的操作符

  • 位操作符

    • 按位非:~

    • 按位与:&

    • 按位或:|

    • 按位异或:^

    • 左移:<<

    • 有符号右移:>>

    • 无符号右移:>>>

  • 布尔操作符

    • 逻辑非:!

    • 逻辑与:&&

    • 逻辑或:||

    布尔操作符中的&&、|| 与 位操作符中的&、|非常类似。但是依然有区别:

    布尔操作符为短路操作,可能不会执行后续操作。

     var a = 10;//1010 var b = 11;//1011 var c = b;//1011 ​ a|b++;//11  将1010和1011进行或运算后的结果 b;//12  b++执行了 ​ a||b++;//10 a转换为true后,直接返回了a的值,不管右边是什么。 b;//11  b++被短路,没有执行

函数

……

本章的各方面介绍都比较基础,在后续章节会有更深层次的讲解。

变量、作用域和内存问题

基本类型和引用类型

基本类型:简单的数据段

引用类型:可能由多个值构成的对象

  • 基本数据类型:

    • Undefined

    • Null

    • Boolean

    • Number

    • String:多数语言将String作为对象,但是JavaScript并不是。

  • 引用类型的值:保存在内存中的对象

参数传递

JavaScript中的参数传递无论是基本数据类型还是引用数据类型均为值传递。

  • 基本数据类型的值传递很好理解,就是新建一个副本将变量值赋给这个局部变量。

  • 引用数据类型的值传递也是新建一个副本,保存该对象的内存地址。它与其它语言的引用传递一样,对该变量的改变会反映在外部变量之上。但是区别在于,当对这个局部变量赋新值时,并不会影响外部变量的值。

     var obj = new Object();     obj.name = 'obj';     var func = (obj)=>{         console.log(obj.name);//obj         obj = new Object();         obj.name = 'obj2';         console.log(obj.name);//obj2              } ​     func(obj)     console.log(obj.name);//obj

检测类型

对于基本数据类型的种类检测,typeof()函数可以很好的胜任,但是对于所有的引用类型和null值,typeof()只能返回Object。这时就需要instanceof操作符。

arr instanceof Array;//true

外源知识点:严格模式的变化

  • 把过失操作(静默失败)转换为异常抛出

    • 无法意外创建全局变量

    • 严格模式会使引起 静默失败 的赋值操作抛出异常,例如:

      NaN = 123;//非严格模式下静默失败;严格模式下抛出异常

    • 删除不可删除的属性时抛出异常(这也是静默失败)

      delete Object.prototype;//抛出异常

    • 函数参数不可重名(普通模式允许重名,会发生覆盖,可以通过arguments来访问)

    • 禁止八进制语法

  • 简化变量的运用

    • 禁用with

    • eval不再为上层的范围引入新变量

    • 禁止删除声明变量

  • 针对eval、arguments的改变

    • eval中不能再对arguments和eval赋值。

    • 参数不会再与arguments保持同步。普通模式下arguments与变量始终保持一致。

    • 不再支持arguments.callee。因为作用小且影响优化。

  • 更加安全

    • 函数中的this不再会被强制转换为对象。

      非严格模式下this在某种情况下指向了全局变量

    • 禁用caller

    • ……+

执行环境以及作用域

执行环境

执行环境定义了变量有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中

在web浏览器中,全局执行环境是window。

每个函数都有自己的执行环境,当执行流进入一个函数,函数的环境就被压入环境栈中(栈中包括其更高层环境),函数执行完毕后,弹出函数环境。

作用域链

代码在一个环境中运行的时候,会创建变量对象的一个作用域链,用来保证对执行环境有权访问的所有变量和函数的有序访问。

作用域链的结构:

  • 作用域链的前端,始终是当前执行代码所在环境的变量对象。

    • 如果这个对象是函数,则将函数的 活动对象 作为变量对象。

  • 作用域链的下一个变量对象,来自下一个包含环境(高层包含环境),以此类推。

  • 全局执行环境的变量对象始终都是作用域链的最后一项。

    这样,局部环境中的代码就可以通过作用域链来访问父作用域的变量。在某个环境中引用一个标识符时,沿着作用域链搜索这个变量。从低到全局。

延长作用域链

有些语句可以在作用域链的前端临时增加一个变量对象,但对象会在相应代码执行完后被移除。

  • try-catch语句的catch块

  • with语句

引用类型

引用类型是一种数据结构,用于将数据和功能组织在一起,也被称为类。

Object类型

创建实例:

  • new操作符

     var person = new Object(); person.name = 'Tom'; person.age = 19;
  • 字面量语法:

     var person = {     name = 'Tom',     "age" = 19//在字面量语法中,属性值可以直接是字面量也可以是字符串形式 }

访问属性:

  • 点表示法

  • 方括号表示法:与点表示法没有什么不同,区别在于可以支持变量访问属性(动态)

基础类型

Array类型

与其他语言的数组对象不同,Array类型的每一项可以保存任意类型的数据,且大小可以动态调整。

创建数组的方法:

var colors = new Array();//new可以省略 
var colors = []

数组的属性length不是只读的,可以通过设置length来扩充数组。

join函数

接受一个参数,调用每一项的toString()函数,将参数作为分隔符,合成一个整字符串。

栈方法&队列方法

可以使用数组提供的方法来模拟不同的数据结构。

栈方法

  • push():接受任意数量的参数,将其添加到数组的末端。

  • pop():取得数组末端的值

队列方法

  • push()

  • shift():取得数组前端的值

因为栈与队列的区别在于先进先出还是先进后出,所以进入函数并没有区别,区别体现在弹出函数是从头部弹出还是从末端弹出。

重排序方法

  • reverse():仅仅就是反转数组

  • sort():按升序排列数组项,但使用的是数组项的toString()方法。

    但是在进行数字排序时,sort()无法跨位比较。于是,sort()方法可以接受一个函数作为参数,一边我们能进行自定义的比较。

操作数组

  • concat():复制一个当前数组的副本,将参数加到副本的末尾(可以是多个参数也可以是一个数组)

  • slice():根据当前数组的一个或多个项创建一个新数组(副本)

    • 一个参数:从当前位置到结尾的全部数组项

    • 两个参数:起始位置、结尾。

    • 外源知识点:在系统提供的函数中,涉及到索引的时候,-1往往代表着倒数第一。

  • splice():替换函数

    • 参数1:起始位置,必选

    • 参数2:删除个数,必选

    • 参数3…:插入的内容

位置方法

  • indexOf()

  • lastIndexOf():两个函数在每一项执行全等比较。

迭代方法

  • every():只有每一项都返回true才会返回true,否则为false

  • some():有任何一项返回true则返回true,否则为false

  • filter():返回 {返回true的项} 组成的新数组

  • forEach():没有返回值

  • map():返回 {返回结果} 构成的新数组

  • 以上的函数均接受一个函数作为参数,对数组中的每一项运行该函数,根据各自不同的条件进行判断。其中作为函数的参数分别为:

    • item

    • index

    • array

归并方法

  • reduce():接受一个函数作为参数,对数组中的每一项执行该函数,并将该函数的返回值作为新的下一个函数的参数。作为参数的函数接受四个参数:

    • prev:传入的初始值为零

    • cur

    • index

    • array

  • reduceRight():就是将归并顺序从后向前了。

Date类型

var date = new Date();//直接创建Date()可以获取当前时间下的时间对象。

生成日期

  • Date.parse():解析字符串格式的日期参数

    Date.parse('2019/12/31');	//1577721600000

  • Date.UTC():解析代表日期的多个参数

    Date.UTC(2019, 11, 31);	//1577721600000

    值得注意的是,UTC()函数中月份的参数是从0开始计算的,也就是1月份为0,2月份为1……。如果参数表示超过了实际范围,比如出现了“12”的月份,不同的浏览器处理方法不同,chrome的做法是,将相应部分替换为了当前的日期的部分。

另外,在构造函数中Date()会自动调用以上两种解析方法,也就是:

var date1 = new Date('2019/12/31');
var date2 = new Date(2019, 11, 31);
var date3 = new Date(1577721600000);//以上均等价

  • Date.now():获取当前时间的毫秒表示。

  • Date.toString():获取当前时间的描述,另Date.toLocalString()

  • Date.valueOf():反复日期的毫秒表示,通常用来比较时间早晚。

相关方法:

  • 格式化方法:to...String..()

  • 时间组件方法:getTime()、setTime()、getMonth()、getDay()……

RegExp类型

正则表达式格式

var expression = / pattern /

flags

  • pattern:表达式的内容

  • flags:模式

    • g:全局模式

    • i:表示不区分大小写

    • m:表示多行模式

另一种语法:var expression = RegExp('pattern', 'flags');

这种语法的好处是,可以使用外部的变量动态定义正则表达式。

语法

实例方法

  • exec():专门为了捕获组而设计的。

    • 返回的是一个Array 的实例,记录匹配到的文本数组,但是有俩个特别属性:index、input

  • test():在模式与该参数匹配的情况下返回true,否则false

  • compile():可以在脚本的执行过程中编译正则表达式,也可以改变已有表达式

构造函数属性

长属性名短属性名说明
input$_最后一次要匹配的字符串
lastMatch$&最近一次的匹配项
lastPaten$+最近一次的捕获组
leftContext$`input字符串中lastmatch之前的文本
rightContext$'input字符串中lastmatch之后的文本
mutiline$*布尔值,表示是否所有表达式都使用多行模式

Function对象

函数是对象

定义函数

function sum(num1, num2){
    return num1 + num2;
}
var sum = function(num1, num2){
    return num1 + num2;
}
var sum = new Function('num1', 'num2', 'return num1 + num2');

参数

  • 形参:函数定义中的参数

  • 实参:实际运行时传入的参数

JavaScript的参数与大部分其他语言的参数都不一样,声明格式中的形参只提供便利,并不是强制的必须,实际调用参数时,可以传入任意比这些多或少的参数。参数最终均存放于arguments中

  • 在非严格模式下,arguments与形参保持一致,改变形参,arguments也会改变

  • 在严格模式下,这种一致不存在了,而且无法改变arguments的值了。

没有重载

因为arguments的存在,函数的形参没有了存在的必要性,因此依靠形参区分的重载机制就不复存在了。

函数声明与函数表达式

  • 函数声明:function func(params);

  • 函数表达式:`{……}

函数解析器对这两个并不是一视同仁,在解析器在向执行环境加载数据时,会率先读取函数声明(状态提升),并使其在执行任何代码前可用,至于函数表达式,只有在执行到该代码行才会真正被执行。

这也就导致了两种函数定于语句的不同结果:

sum(1, 2);//正常运行,因为sum被声明为一个函数,经历了函数声明提升过程
function sum(num1, num2){
    return num1 + num2;
}
sum(1, 2);//报错func is not a function,在代码提升阶段,sum只是一个变量,还没有赋值为function
var sum = function(num1, num2){
    renturn num1 + num2;
}

外源知识点:JavaScript执行过程

基础知识点:

  • JavaScript时单线程的,正常情况下(不主动开启新线程时

  • -----),一个页面只有一个线程

  • JavaScript时异步执行的,通过Event Loop(时间循环)实现

执行环境主要有以下三种

  • 全局环境

  • 函数环境

  • eval()

每当进入一个环境都会创建一个相应的执行上下文,js引擎以栈的方式对这些上下文进行处理(函数调用栈),栈底永远是全局上下文,栈顶永远是当前执行上下文(结合作用域链来理解)

  1. 编译阶段:编译器

    1) 词法、语法分析:

    在代码加载完毕后执行

    2) 预编译阶段:每当进入一个新的上下文环境中时,都要进行一次预编译和执行

    • 函数调用栈

    • 创建执行上下文

      1. 创建变量对象

        • 创建arguments对象,仅在函数环境中进行,全局环境没有此过程

        • 检查当前上下文的函数声明,按代码顺序查找,属性值为函数所在堆内存地址的的引用,可以被创建,存在则覆盖

        • 检查当前上下文的变量声明,按代码顺序查找,将找到的变量提前声明。不存在就创建,存在则掠过

        需要注意的是,在全局环境中,window对象就是全局上下文的变量对象,所有的变量和函数都window对象的属性和方法

      2. 建立作用域链

        • 作用域链的栈顶永远是当前的作用域

        • 作用域链的栈底永远是全局作用域

        • 作用域链保证了变量和函数的有序访问,方法是沿着作用域链从顶到底的查找变量和函数,找到就停止,一直到底(全局作用域)也没找到就抛出引用错误。

      3. 确定this指向

  2. 执行阶段:引擎

    1) 进入执行上下文的声明周期

    2) 代码执行

    3) 垃圾回收

函数内部属性

函数内部有两个特殊的对象

  • arguments:类数组对象,存储传递来的参数。但这个对象还有别的属性:

    • callee:指针,指向拥有这个arguments 对象的函数。早期ESMAScript不允许使用命名函数表达式,导致不能实现递归操作,于是,加入了arguments.callee属性,ESMA5的严格模式已经禁用arguments.callee属性。

  • this:this引用的是函数运行的环境对象。

函数的属性和方法

每个函数都包含两个属性:length、prototype

  • length:表示函数希望接收到的参数个数

  • prototype:在后面的继承中会有详解

    • apply()

    • call()

      这两个函数用于在特定的环境(作用域)中调用母函数。

          var obj = {
              name : "job"
          }
          var func = function(){
              console.log(this.name);
          } 
          func.apply(obj);//job
      

      区别在于,apply接收this与数组,call接收this与参数列表

    • bind():这个方法会创建一个函数的实例,其this值会绑定到传给bind函数的值

          var obj = {
              name : "job"
          }
          var func = function(){
              console.log(this.name);
          } 
          var a = func.bind(obj);
          a();//job
      

      这三个函数本质上都是在操作this或者是作用域、执行环境

基本包装类型

为了便于操作基本类型值,ECMAScript提供了三个引用类型,每当读取一个基本类型的时候也,后台就会创建一个对应的基本包装类型的对象,从而让我们能调用一些方法来操作这些数据:

当读取到一个字符串,发生以下过程:

  • 创建String类型的一个实例 var str = new String('abc');

  • 在实例上调用指定的方法

  • 销毁这个实例

应用类型和基本包装类型的主要区别就在于声明周期,前者的生命周期为当前作用域,而后者的生命周期为当前此代码行。

因此也就可以解释一些奇怪的行为:

var str = 'abc';
str.color = 'red';

str.color;//undefined,因为这个str的对象与上面的已经不同了,及时销毁。

也可以显式的创建基本包装类型的对象,实现上面的操作,但不要随意这样做。

同时,使用new创建的基本包装类型的实例 与 通过调用同名的 转型函数 是不一样的

var value = 2;
var num = Number(value);//转型函数
var obj = new Number(value);//通过new创建的新实例

Boolean类型

没有太多的作用,反倒容易与基本类型混淆:

    var trueValue = true;
    var falseObject = new Boolean(false);

    console.log(falseObject&&trueValue);//true,因为falseObject是一个指向Boolean的指针,在逻辑运算中自动转换为true

Number类型

重写了toString(),可以接受一个参数表示数值的基数

提供了一些格式化表示数值的方法:

  • toFixed():接受一个参数,返回相应数量的小数位的字符串形式,四舍五入。

  • toExponential():返回以指数表示法表示的数值的字符串形式

  • toPrecision():可能返回固定大小(fixed)格式,也可能返回指数(exponential)格式,接受一个参数表示所有数字的位数。

String类型

  • charAt():接受参数index,返回给定位置的字符

  • charCodeAt():接受参数index,返回给定位置的字符编码

  • concat():将多个字符串拼接

  • 分割字符串方法:不会改变字符串,返回一个新的子字符串

    • slice()

    • substr()

    • substring()

  • 字符串位置方法:接受一个字符串参数,返回在字符串中的位置(没找到就是-1)

    • indexOf()

    • lastIndexOf():返回最后一个值

  • trim():创建一个字符串副本,删除前置后置所有空格

  • 大小写转换:

    • toLowerCase()

    • toUpperCase()

  • 模式匹配:

    • match():在字符串上调用这个方法,本质上可在RegExp对象上调用exec()方法一样。接受一个正则表达式或者RegExp对象,返回array,有两个特殊属性:input、index

    • search():返回位置索引或-1

    • replace():接受两个参数,正则表达式(或字符串)和准备替换上的字符串(也可以是每次匹配都调用的函数)。

  • localeCompare():比较字符串,参数靠前返回1,参数靠后返回-1,一样返回0

  • fromCharCode():接受字符串编码,转换为字符串。

单体内置对象

Global对象

Global对象是一个兜底对象,它存在于作用域链的底端,是所有环境的外部环境。

Math对象

只介绍部分方法:

  • 最大值、最小值:

  • 舍入方法:

    • Math.ceil():执行向上舍入(进一)

    • Math.floor():执行向下舍入(完全舍去)

    • Math.round():四舍五入

  • 随机数:random():返回大于等于0,小于一的一个随机数(浮点数)。


面向对象的程序设计(对象与原型链)

理解对象

对象:无序属性的集合,其属性可以包含基本值、对象或者函数

属性类型

数据属性

这些特性是为了实现JavaScript引擎用的,在JavaScript中不能直接访问他们,为了表示这一含义,使用[[…]]表示法:

  • [[Configurable]]:表示能否重新定义,包括删除、修改属性名称等

  • [[Enumerable]]:表示能否迭代(for-in)循环

  • [[Writable]]:表示能否修改属性值

  • [[Value]]:这个属性的属性值本身

以上属性值 默认为true

要设定属性的特性,需要使用Object.defineProperty()

 var person = {} Object.defineProperty(person, 'name', {     writable : false,     value : 'Tom', })     console.log(person.name);//Tom person.name = 'Jack'; console.log(person.name);//Tom,修改属性失败,严格模式下报错

使用defineProperty()定义新的属性时,writable 和 configurable 默认为false,即不可修改的。

访问器属性

访问器属性不包含属性值,他们包含一对getter和setter函数(非必须),读取访问器属性时,调用getter函数,给访问器属性赋值时,调用setter函数。访问器属性有如下 [[特性]]

  • [[Configurable]]:能否编辑该属性,包括删除和修改属性名

  • [[Enumerable]]:能否迭代循环(for-in)

  • [[Get]]:读取时调用

  • [[Set]]:赋值时调用

getter和setter特性默认值均为undefined。

定义访问器属性同样也需要Object.definePropoerty()

 var person = {     _age:0 } Object.defineProperty(person, 'age', {     get: function(){         return this._age;     },     set: function (param) {         this._age = (param>10&&param<20)?param : this._age;     } }) person.age = 12; person.age = 22; console.log(person.age);//12

读取属性的特性

Object.getOwnPropertyDescriptor()

 var person = {     _age:0 } Object.defineProperty(person, 'age', {     get: function(){         return this._age;     },     set: function (param) {         this._age = (param>10&&param<20)?param : this._age;     } }) var descripor = Object.getOwnPropertyDescriptor(person, 'age'); ​ console.log(descripor.configurable);//false,不要忘记访问器属性的特性默认为false

创建对象

工厂模式

写一个创建对象的函数,在需要创建对象时,直接调用函数就可以。一个模型适用于一组相似对象,省去很多代码量。

 var person = function(name, age, job){     var o = new Object();     o.name = name;     o.age = age;     o.job = job;     o.sayName = function(){         console.log(this.name);     }     return o; }
缺点:

无法判断对象类别(通过instanceof、isPrototyOf判断类型)

构造函数模式

 var Person = function(name, age, job){     this.name = name;     this.age = age;     this.job = job;     this.sayName = function(){         console.log(this.name);     } }//没有return语句 var per1 = new Person() var per2 = new Person()

实际上,构造函数就是一个函数,当在后面使用new操作符使用时,它才被当作构造函数来创建对象。另外,构造函数的函数名约定首字母大写,与普通函数做区分。

使用new操作符 创建新实例 过程:

  • 创建一个新对象

  • 将构造函数的作用域赋给新对象(因此this指向这个新对象)

  • 执行构造函数的代码(为新对象添加属性)

  • 返回新对象

per1和per2分别保存着person的不同的实例,这两个实例都有一个constructor属性,指向了Person。

优缺点:
  • 优点:通过构造函数方法创建的对象可以通过instanceof操作符分辨类型

  • 缺点:每个方法都要在每个实例上重新创建一遍,这是没有必要的,还会导致同名函数在不同实例上的作用域链和标识符解析不同。

原型模式

我们创建的每一个函数都有一个prototype(原型)属性,这是一个指针,指向一个对象,用来包含可以有特定类型的所有实例共享的属性和方法。prototype相当于是相同对象类型,不同实例间的共享仓库。

每当创建一个函数(构造函数),都会自动为其生成一个prototype属性,指向原型对象;而原型对象也有一个constructor属性指向原来的函数。相互索引。

实例的prototype属性是隐藏的(内部属性)。

isPrototypeOf() 新的检测类型函数

前面说过,instanceof操作符通过constructor来检测实例类型,isPrototypeOf()函数则是通过prototype指针来判断类型。

另外,Object.getPrototypeOf()函数可以直接返回实例的prototype内部属性。

原型查找

当代码读取某个对象某个属性时,都会执行一次搜索,目标时具有给定名字的属性,先搜索实例本身,如果没有找到,则沿着原型指针搜索原型。

构造函数模式中,执行构造函数会在实例中添加一些属性,而在原型模式中,实例本身并没有添加属性,但是通过指针查找原型(公共仓库),还是找到了相应的属性。

正因这一特性,原型具有动态特性。原型下的实例可以与原型保持动态一致,无论什么时候改变原型,只要指针不变,实例就能享有改变。

hasOwnProperty() 检测一个属性是在实例中还是原型中。

 per1 = new Person(); per1.hasOwnProperty('name');//true

在构造函数中定义的属性是实例独有的,不共享的,存于实例本身的。

in 操作符与原型

 'name' in per1;//true

判断一个实例是否具有某属性(不管是在是实例中还是在原型中)

与hasOwnsProperty()函数相结合可以判断一个属性是否属于原型中的。

for-in循环一个实例时,返回的是实例所有可枚举的([[Enmuerated]]为true)属性。既包括实例本身的属性,也包括实例的原型的属性

Object.keys() 方法

keys() 接受一个对象作为参数,返回该对象的所有可枚举属性的字符串数组

Object.getOwnPropertyNames() 方法

同样接受一个对象作为参数,返回所有的可枚举属性,包括constructor。

使用原型的两种方法
  • 单独为了每一个属性赋值,并不改变整体原型的指向

 function Person(name, age, job){     this.name = name;     this.age = age;     this.job = job; } Person.prototype.toString = function(){         alert('hello')     } 
  • 直接将原型对象整个赋值,改变了property指针的指向。这样更快,但是同时改变了原型对象中constructor的指向(新的对象constructor指向Object)。

 function Person(name, age, job){     this.name = name;     this.age = age;     this.job = job; } Person.prototype = {     toString : {         alert('hello')     } }

当函数创建时,自动生成property对象,此时的对象的constructor默认指向原函数,但是若是改变这一个函数,函数中的constructor自然就改为指向Object了。

优缺点:
  • 优点明显,就是可以共享数据与函数,还有动态特性

  • 缺点:也还是共享,没有私有数据。同时,无法通过构造函数进行自定义的初始化。

组合使用

结合构造函数与原型的优点,同时使用两种方法。

把所有信息封装在构造函数中,又在构造函数中初始化原型

与后面的动态原型模式类似……

动态原型模式

 function Person(name, age, job){     this.name = name;     this.age = age;     this.job = job; ​     if (this.sayName == undefined) {         Person.prototype.sayName = function(){             console.log(this.name);                      }     } } ​ var per1 = new Person('Tom', 18, 'engineer'); per1.sayName(); ​

之所以在判断中为原型赋值,是因为原型的初始化后就不需要再赋值了,将一直存在。

优缺点
  • 优点:保留了构造函数和原型两方的优点,可以判断类型,共享,私有等

寄生构造函数模式

这是一种特殊情况下的使用方法,与工厂模式类似,但使用了new操作符:

function SpecialArray(){
    var arr = new Array();
    arr.push(...arguments);
    arr.res = function(){
        return this.join(',')
    }
    return arr;
}
var colors = new SpecialArray('red', 'green');
console.log(colors.res());

这里的构造函数有return 返回值,特地说明一下不同:

  • 没有return返回值:默认创建一个新对象实例

  • 有return返回值:返回该给定实例

这种寄生构造函数模式就是通过寄生在一个已有的构造函数上,构建一个新的对象,可以实现无损扩充系统函数的效果。

稳妥构造函数模式

稳妥对象:没有公共属性,而且其方法不引用this的对象,适合在一些安全环境下使用。

继承与原型链

继承在面向对象语言中主要分为接口继承实现继承,而ECMAScript只支持实现继承(通过原型链实现)。

原型链继承

 function Parent(){     this.name = 'parent'; } Parent.prototype.say = function(){     console.log(this.name); } var Child = function(){     this.name = 'Child'; } Child.prototype = new Parent(); ​ var child = new Child(); console.log(child.name); Child.prototype.say(); child.say(); ​

child继承parent是通过将prototype设为parent的实例实现的

实现的本质是将父亲对象的方法存入子对象的原型中。

 function Parent(){     this.name = 'parent'; } Parent.prototype.say = function(){     console.log(this.name); } var Child = function(){     this.name = 'Child'; } Child.prototype = new Parent(); child = new Child(); child.say();//child

实例、构造函数、原型的继承关系

需要注意的是,在将prototype赋值为Parent的实例,再创建的Child的实例的constructor均指向Parent,当然,再那之前创建的实例是指向Child的。这里prototype的赋值使前后的实例”撕裂“,不再属于同一类型。

确定原型和实例的关系
  • instanceof操作符

  • isPrototypeOf()方法:只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。

优缺点
  • 优点:方便

  • 缺点:原型的共享问题,以及没法单独自定义实例化

借用构造函数式继承

思路就是在子类型的构造函数内部调用父类型的构造函数(apply、call)

function Parent(name){
    this.name = name;
}
function Child(){
    Parent.apply(this, arguments);
}
var child = new Child('Tom');

console.log(child.name);
优缺点:

这根本不是一个成熟的方法,因为没有使用原型链,继承名不副实。

组合继承

广泛使用的继承方法,将借用构造方法和原型链方法结合起来,即通过i原型上定义的方法实现了函数的复用,又能够保证每个实例都有自己的属性。

function Parent(name){
    this.name = name;
}
Parent.prototype.say = function(){
    console.log(this.name);
}

function Child(name, age){
    Parent.apply(this, arguments);
    this.age = age;
}
Child.prototype = new Parent();
var child = new Child('Tom', 12);

console.log(child.age);//12 
child.say();//Tom

缺点:重复声明父类中的属性,造成内存浪费。

原型式继承

使用Object.create()来创建一个新的对象,这个新对象的prototype指向传入的参数(旧对象)

 var Parent = {     name : "Parent",     say : function(){         console.log(this.name);     } } var child = Object.create(Parent); child.name = 'Child'; console.log(Object.getPrototypeOf(child));//{name: "Parent", say: ƒ} child.say();//Child console.log(child.constructor);//ƒ Object() { [native code] }

这种方法省去了搭建子类构造函数的过程 ,直接将一个实例当作子类型的原型相当于Child.prototype = Parent

这种方法同组合式方法相比省去了很多步骤,同样的,也阉割了很多功能:

  • 组合式

    • 有单独构造函数

    • 为子类型的原型赋值为父类型的一个实例

    • 因为构造函数齐全,子类实例的constructor指向Child子类

    • 可以使用instanceof

  • 原型式

    • 子类型和父类型都不需要构造函数

    • 父类型的实例直接作为子类型的实例的原型(内部属性)

    • 因为没有涉及到构造函数,子类实例的constructor指向Object

    • 不能使用instanceof

寄生式继承

组合式继承的缺点

寄生式继承就是通过一个中间实例接承父类的prototype,从而省去了父类属性的重复声明。

寄生组合式继承

……

总结

虽然本章的内容很多,介绍了各种各样的方法,但是实际上是在循序渐进的引导出一种或两种成熟的,适合大多数情况的方法。引导过程对于加深对原型的理解很有帮助,但是没有必要反复去看。

本章中有三个重要概念:

  • 实例:就是引用类型,包括属性以及函数,可以通过各种方法生成,如字面量生成,构造函数生成等

  • 构造函数:专门用来生成实例的函数,其实就是目的特别的函数(认识到构造函数就是函数)。需要搭配new操作符使用。new的作用:创建新对象、将对象的作用域赋给构造函数(以便使用this)、运行构造函数、返回对象。

  • 原型:是构造函数的一个属性,是实例的一个内部属性(无法直接访问),这些属性指向原型这个对象。原型中有一个属性constructor又指向具有他的构造函数,这样就形成了一个封闭链条。

创建对象方面,其实常用的就是组合式。也就是将构造函数和原型结合起来,兼顾二者的优点。

     function Person(name){         this.name = name;     }     Person.prototype.say = function(){         console.log(this.name);     }     var person = new Person('Tom');     person.say();

当我们创建一个函数时,会自动生成其prototype属性对象,这个对象的constructor是回指向函数的。

构造函数:创建的对象的constructor正是指向构造函数;实例对象的属性是独占的,不共享的

原型:原型通过引用原型链实现,同一对象的不同实例间共享数据,通常定义些方法。

继承时,依然保留组合式的传统,兼顾构造函数和原型的优点:

     function Person(name){         this.name = name;     }     Person.prototype.say = function(){         console.log(this.name);     } ​     function Child(name){         Person.apply(this, arguments);     }     Child.prototype = new Person(); ​     var child = new Child('Tom');     child.say();

当然也可以使用Object.create()来偷懒。

函数表达式

首先回顾一下,创建函数的两种方法:

  • 函数声明:

    function person(){} 这种方法会引发函数声明式提升,也就是可以在声明前调用函数。

  • 函数表达式:

    var person = function(){} 这种方法没有函数声明提升,所以在之前调用会引发空指针错误(var person 变量提升,但是该变量没有赋值)

递归

递归函数是在一个函数中通过名字调用自身实现的

通常,使用函数表达式和arguments.callee来进行调用。

闭包

闭包:有权访问另一个函数作用域中的变量的函数。

创建闭包的常见方式:在一个函数中创建另一个函数。

     function person(name){         return function say(){             console.log(name);         }     }     person('Tom')();//Tom


闭包能访问外部函数变量的原因:

正常情况下,创建一个函数,会自动生成其作用域链,作用域链的末端就是当前执行环境的活动对象。当退出当前环境时,该活动对象没有指针索引,会被自动回收。

当出现闭包时,退出当前环境,但是闭包保存了下来,依然有指针指向该活动对象,于是,……

闭包与变量

闭包保存的是在函数退出该作用域时保存的活动对象(运行现场),而不是闭包创建时的活动对象。

    function person(){
        var name = 'Tom';
        var say = function(){
            console.log('hey',name);
        }
        name = 'Bob';
        return say;
    }
    person()();

在创建say函数时,name是Tom,但是当退出person作用域时,name已经变成了Bob。闭包通过索引访问变量,具有动态特性。

闭包中的this对象

var name = 'Window';

var o = {
    name : 'Object',
    getName : function(){
        return function(){
            return this.name;
        }
    }
}
console.log(o.getName()());//Window

每个函数在调用的时候,都会自动获取两个特殊变量:arguments和this,内部函数在搜索这两个变量的时候只会搜索到本身的活动对象截至,所以this和arguments在闭包中均无法渗透。

模仿块级作用域

function parent(){
    (function(){
        var words = 'Tom';
        console.log(words);//Tom
    })()
    console.log(words);//ReferenceError
}
parent();

(function{……})()中形成了一个块级作用区,拥有独自的作用域

私有变量

通过函数的独自作用域和闭包来形成私有变量.