1. JS简介
1995年诞生,作者布兰登·艾奇 Brendan Eich
为了处理输入验证操作,减少不必要的数据交换,节省带宽资源
为什么会有js标准化?
Netscape拥有Javascript,微软为了避开与Netscape有关的授权问题,微软实现自己的Javascript,命名为JScript,实现上有差异,导致后来很多的兼容性问题。
ECMA-欧洲计算机制造商协会
TC39-39号技术委员会
Technical Committee #39ECMAScript是JavaScript的子集,也就是说JavaScript包含ECMAScript。
P.S. TypeScript是JavaScript的超集,TS是JS的一个重要方向,很多大型库都是用TS写的。
JavaScript = ECMAScript + DOM + BOM
核心:ECMAScript
文档对象模型:DOM
浏览器对象模型:BOM
Web浏览器只是ES实现的宿主之一,并无绑定关系,其他宿主:Node、Adobe Flash等。
宿主环境不仅提供基本的ES实现,同时也会提供该语言的扩展,以便语言与环境之间的对接交互。
ECMAScript
ECMA-262——新脚本语言的标准
ECMA-262 edition | 改动 |
1st edition | JS 1.1 |
2nd edition | 编辑加工 |
3rd edition | 真正修改,里程碑,真正的编程语言 |
4th edition | es6的前身,跨度大,被放弃 |
5th edition | ECMAScript 3.1 |
6th edition | ... |
文档对象模型 DOM
用于HTML的应用程序编程接口 API,DOM把整个页面映射为一个多层节点结构。
文档树。
页面 -> 节点 -> 数据
作用:借助DOM提供的API,可以轻松实现删除、添加、替换或修改任何节点。
之所以会有DOM,也是因为Netscape和微软两强割据,浏览器互不兼容,W3C才规划推出DOM。
DOM级别
- DOM1级(两个模块)
-
- DOM核心——规定如何映射基于XML的文档结构,以方便访问和操作文档内容
- DOM HTML——在核心基础上扩展,添加针对HTML的对象和方法
- DOM2级
-
- DOM视图
- DOM事件
- DOM遍历和范围
- DOM样式——对CSS的支持(通过对象接口添加)
- DOM3级
-
- DOM加载和保存模块——以统一方式加载和保存文档的方法
- DOM验证模块——新增验证文档的方法
浏览器对象模型 BOM
BOM只处理浏览器窗口和框架,外加JS扩展。
2. HTML+JS
这个js文件可以被同域访问,也可以被跨域访问。
一般情况下,所有元素都会按照在页面中出现的先后顺序依次解析。所以不加defer、async属性时,把元素放在页面主要内容最后。
延迟脚本:使用 defer属性 可以让脚本在文档完全呈现之后再执行。延迟脚本总是按照指定它们的顺序执行。
异步脚本:使用 async属性 可以表示当前脚本不必等待其他脚本,也不必阻塞文档呈现。不保证顺序执行。
可以指定在不支持脚本的浏览器中显示的替代内容,启用脚本时,不显示其中的内容。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yNjhDNidTZjRjNiVjZxYWYwADN0YmY3ETMwIjMyAjM38CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
原图地址:
async-vs-defer-attributes3. 语法、数据类型、流控、函数
值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。
引用数据类型:对象(Object)、数组(Array)、函数(Function)。
注:Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。
函数
严格模式对函数的一些限制:
- 不能把函数命名为eval或arguments
- 不能把参数命名为eval或arguments
- 不能出现两个命名参数同名的情况
理解参数
ECMAScript中的参数在内部是用一个数组来表示的,故函数不限制传递参数的个数。
通过arguments对象来访问这个参数数组,从而获取传递给函数的每一个参数。
arguments对象是类数组,并非Array的实例。可以像数组那样使用[]取值,也可以使用length获取长度。
命名参数——定义函数时,参数中的名称。命名的参数只提供便利,但不是必需的。
它的值永远与对应命名参数的值保持同步。
arguments对象中的值会自动反映到对应的命名参数,所以修改arguments[1],也就修改了num2.
没有传递值的命名参数将自动被赋予undefined值。
ECMAScript中的所有参数传递的都是值,不可能通过引用传递参数。
无须指定函数的返回值,未指定时返回undefined值。
ECMAScript不能重载。
4. 变量、作用域和内存问题
变量
基本类型值
简单的数据段
5种基本数据类型:Undefined、Null、String、Number、Boolean
ES6引入了一种新的原始数据类型 Symbol,所以最新的JS应该是6种基本数据类型。
保存在变量中,为实际的值。
变量复制,实质是创建一个新变量,并把原值复制过去。
在内存中占据固定大小的空间,因此被保存在栈内存中。
引用类型值
多个值构成的对象,保存在内存中,JS不允许直接访问内存中的位置,即不能直接操作内存空间。
引用类型的值是按引用访问的。
对于引用类型的值,可以增删属性和方法。
变量复制,也是两个变量对象,两个(栈?)空间,不过两个变量对象存储的值是同一个值(这个值是同一个指针,指向存储在堆内存中的同一个对象)。
引用类型的值是对象,保存在堆内存中。
传递参数
所有函数的参数都是按值传递的。传参如同变量复制,传基本类型值,为实际值;传引用类型值,为指针值。
function setName(obj){
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
console.log(person.name);
即使在函数内部修改了参数的值,但原始的引用仍然保持不变。当在函数内部重写obj时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。
类型检测
typeof——检测变量是否基本数据类型
结果:string、number、boolean、undefined、object
typeof null; // object
typeof {}; // object
typeof /a-z/g; // "object"
instanceof——检测是什么类型的变量
true: 是给定引用类型的实例;
基本类型不是对象,会返回false;
o instanceof O; // 变量o是O的实例
作用域
执行环境
执行环境是JS中最为重要的一个概念!
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的一个执行环境。web浏览器中,全局执行环境是window对象。
执行环境有全局执行环境和函数执行环境之分。
每个函数都有自己的执行环境。
当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,将控制权返回给之前的执行环境。
深入理解JS作用域作用域链 scope chain
变量对象的作用域链
用途:保证对执行环境有权访问的所有变量和函数的有序访问。
作用域链的前端,始终都是当前执行的代码所在环境的变量对象。
下一个变量对象来自下一个包含环境,依次扩大范围......
作用域链的终端,全局执行环境的变量对象,如window对象。
标识符解析是沿着作用域链一级一级地搜索标识符的过程。从前到后,从小范围到大范围。
内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境的任何变量和函数。即:能由小访问大,不能由大直接访问小。
这些环境之间的联系是线性的、有次序的。
查询标识符:找到或到顶为止。
变量的执行环境有助于确定应该何时释放内存。
JS深入之作用域链延长作用域链
- try-catch语句的catch块
- with语句
块级作用域
ES6之前,js不存在块级作用域,var声明的变量会被提升到执行环境的顶部。
let、const出现之后,才有了块级作用域。
垃圾收集
JS具有自动垃圾收集机制。
原理:找出那些不再继续使用的变量,然后释放其占用的内存。垃圾收集器会周期性的执行这个动作。
对于不再有用的变量打上标记。
标记清除
JS最常用的垃圾收集方式是标记清除。
标记“进入环境”、标记“离开环境”
将内存中变量都加上标记-去掉引用的变量的标记=待清除的标记
引用计数
存在循环引用的问题时,该算法会导致问题。
解决方法:手动断开变量与它此前引用的值之间的联系,将变量手动置为null,即解除引用,解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
V8引擎详解-垃圾回收机制5. 引用类型
引用类型有时被称为对象定义,它们描述的是一类对象所具有的属性和方法。
引用类型是一种数据结构,用于将数据和功能组织在一起。
对象:引用类型的值,引用类型的一个实例。
构造函数:处于创建新对象的目的而定义的一种特殊函数。
var person = new Person();
创建Person引用类型的一个实例,然后把该实例保存在了变量person中。
原生引用类型:5
- Object
- Array
- Date
- RegExp
- Function
基本包装类型:3
- Boolean
- Number
- String
单体内置对象:2
- Global
- Math
创建实例的2种方式:
-
new Object();
- 对象字面量
,简化创建包含大量属性的对象的过程{属性名:值}
访问对象属性,优先使用
.
,特殊情况使用
[prop]
数据的有序列表,动态扩容,存储类型不限。
创建方式同Object:
-
new Array();
-
[values]
检测数组
对于一个全局作用域而言,使用
value instanceof Array
就可以,但对于存在多个框架,存在两个及以上不同的全局执行环境,存在两个及以上不同版本的Array构造函数,从一个框架向另一个框架传入一个数组,这里就有判断的问题了。
Array.isArray(val)
: 最终确定val到底是不是数组。
转换方法
- toLocaleString()
- toString()
- valueOf()
- join()
[1,2,3,4].toString() // "1,2,3,4"
[1,2,3,4].valueOf() // [1, 2, 3, 4]
['1','2','3'].valueOf() // ["1", "2", "3"]
['1','2','3'].join(',') // "1,2,3"
栈方法
后进先出 LIFO
- push(a,b,c,...)——推入数组末尾,返回新数组长度,支持一次推入多个
- pop()——移除最后一项,返回移除的项,无参
队列方法
先进先出 FIFO
- shift()——移除最前一项,返回移除的项,无参
- unshift(a,b,c,...)——推入数组头部,返回新数组的长度,支持一次推入多个
头部 | 尾部 | |
添加 推入 | unshift() | push() |
移除 弹出 | shift() | pop() |
同色配合可模拟出队列:头进尾出、头出尾进
重排序方法
都改变原数组,返回新数组
- reverse()
- sort([compare()])
比较函数compare:接收两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等则返回0,如果第一个参数应该位于第二个之后则返回一个正数。
// 正序排列,小在前,大在后
function compare(val1, val2){
if(val1 > val2){
// 第一个参数较大,应该位于第二个之后
return 1;
}else if(val1 < val2){
// 第一个参数较小,应该位于第二个之前
return -1;
}else{
// 相等
return 0;
}
}
更简单的比较函数,适用于数值类型或valueOf()返回数值类型的对象类型:
// 正序
function compare(val1, val2){
return val1 - val2;
}
// 降序
function compare(val1, val2){
return val2 - val1;
}
// 例子
var arr = [1,4,2,56,34,21,9,34];
arr.sort((val1,val2)=>val2-val1); // [56, 34, 34, 21, 9, 4, 2, 1]
操作方法
不改变原数组,操作副本
- concat()——复制副本,再拼接
- slice(start,end)——子数组
const arr = [1,2,3,4,5,6];
arr.slice(-2,-1); // [5],负数相当于 各自+arr.length
arr.slice(4,5); // [5]
arr.slice(5,3); // []
- splice——主要用于向数组中部插入项
改变数组
const arr = [1,2,3,4,5,6,7];
// 删除2项 (起始位置,删除项数)
arr.splice(0,2);
// 插入 (起始位置,删除项数,插入项)
arr.splice(2,0,"red","green");
// 替换 (起始位置,删除项数,插入项)
arr.splice(2,1,"red","green");
位置方法
- indexOf()
- lastIndexOf()
迭代方法
都不改变原数组
- every()
- filter()
- forEach()
- map()
- some()
归并方法
- reduce((prev, cur, index, arr)=>{})——从第一项开始,逐个遍历到最后
- reduceRight((prev, cur, index, arr)=>{})
- Date.parse()
- Date.UTC()
- Date.now()
new Date(); // Sat Mar 07 2020 21:31:52 GMT+0800 (中国标准时间)
new Date(2007,01,01); // Thu Feb 01 2007 00:00:00 GMT+0800 (中国标准时间)
new Date().toString(); // "Sat Mar 07 2020 21:32:23 GMT+0800 (中国标准时间)"
new Date().valueOf(); // 1583587952694
Date.now(); // 1583587963393
Date.parse('2020-03-08'); // 1583625600000
- toDateString()
- toTimeString()
- toLocaleDateString()
- toLocaleTimeString()
- toUTCString()
日期/时间组件方法
- getTime()
- getFullYear()
- getMonth()
- getDate()
- getDay()
- getHours()
- getMinutes()
- getSeconds()
var date = new Date();
date.getFullYear(); // 2020
date.getMonth()+1; // 3
date.getDay(); // 6
date.getDate(); // 7
date.getHours(); // 21
date.getMinutes(); // 39
date.getSeconds(); // 14
var expr = /pattern/flags
flags:
- i —— 不区分大小写
- m —— 多行模式
- g —— 全局模式
exec()
专门为捕获组而设计,返回包含第一个匹配项信息的数组。
pattern.exec(str);
设置全局标志(g)时,每次调用会查找新匹配项;不设置全局标志则总返回第一个匹配项的信息。
test()
适用于只想知道目标字符串于某个模式是否匹配,常用在if语句中。
const reg = /a-zA-Z0-9/g;
reg.test('849ifu34hf89w9h39');
每个函数都是Function类型的实例。
函数是对象,函数名实质也是一个指向函数对象的指针。
一个函数可能会有多个名字。
函数声明与函数表达式
- 函数声明
- 函数表达式定义函数,不需要函数名
区别?
函数声明也存在变量提升现象,即解析器会先读取函数声明,并使其在执行任何代码之前可用。
函数表达式则必须等到解析器执行到它所在的代码行,才会真正被解释执行。
函数可以作为值被传递
函数内部属性
callee
指针,指向拥有这个arguments对象的函数
arguments.callee()
this
引用的是函数执行的环境对象。
函数的名字仅仅是一个包含指针的变量而已。
caller
这个属性中保存着调用当前函数的函数的引用。
函数属性和方法
属性
length——参数的个数
prototype——保存实例方法的所在,不可枚举
方法
- call(this, a,b,c,...)
- apply(this, [a,b,c,...])
真正强大的地方是能够扩充函数赖以运行的作用域。
最大好处,对象不需要与方法有任何耦合关系。
window.color = "red";
const o = {color: "blue"};
function sayColor(){
alert(this.color);
}
sayColor();
sayColor.call(this);
sayColor.call(window);
sayColor.call(o);
- bind(this)
这个方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值。
基本包装类型
基本类型值不是对象,从逻辑上不应该有方法。
let s1 = "something";
const s2 = s1.substr(2);
后台操作:
// 创建String类型的一个实例
let s1 = new String("something");
// 在实例上调用指定的方法
const s2 = s1.substr(2);
// 销毁这个实例
s1 = null;
不能在运行时为基本类型值添加属性和方法,因为自动创建的基本包装类型的对象,只存在于一行代码的执行瞬间。
引用类型于基本包装类型的主要区别就是对象的生存期。
绿色表示对原值无影响
- charAt(index)——返回给定位置的字符
- charCodeAt(index)——给定位置字符的字符编码
- String.fromCharCode()——字符编码转字符串
- concat()
- slice(start, end)——负值+length
- substr(start, num)——第一个负值+length,第二个负值=0
- substring(start, end)——所有负值均=0
let val = "Hello world";
// substr 负数=>0,args=个数
val.substr(3,-4); // ""
val.substr(3,0); // ""
val.substr(3,1); // "l"
// substring 负数=>0,正反向都可截取
val.substring(-3); // "Hello world"
val.substring(-3,4); // "Hell"
val.substring(3,-4); // "Hel"
// slice 负数+length,不能反向截取
val.slice(-3,-1); // "rl"
val.slice(11-3,11-1); // "rl"
val.slice(-3, 7); // ""
val.slice(-3, 9); // "r"
- trim()
- toUpperCase()
- toLowerCase()
- text.match(pattern)——同pattern.exec(text),返回数组
- text.search(pattern)——index
- text.replace(pattern, str)
- text.split(sep, length)——分隔符,数组长度
let texts = "cat, bat, sat, fat";
const result = texts.replace(/(.at)/g, "word ($1)");
console.log(result); // word (cat), word (bat), word (sat), word (fat)
单体内置对象
Global对象
所有在全局作用域中定义的属性和函数,都是Global对象的属性。
- isNaN()
- isFinite()
- parseInt()
- parseFloat()
url编码
用特殊的UTF-8编码替换所有无效的字符,从而让浏览器能够接受和理解。
- encodeURI()
- encodeURIComponent()——对URI中的某一段进行编码
encodeURI()不会编码本身属于URI的特殊字符;
encodeURIComponent()会编码任何发现的非标准字符;
- decodeURI()
- decodeURIComponent()
强大的函数 eval(str)
window对象
web浏览器将全局对象Global作为window对象的一部分加以实现。
Math对象
属性:
Math.PI 等
方法:
- min()
- max()
- ceil()——向上舍入
- floor()——向下舍入
- round()
- random()
- abs()
- sqrt()
- pow()
6. 面向对象编程
OO:类与对象
对象是一组没有特定顺序的值。对象的每个属性和方法都有一个名字,而每个名字都映射到一个值。
理解对象
属性类型
特性,用于描述属性的各种特征。为实现js引擎而用,不能直接访问它们。
特性是内部值,表示法:[[Enumerable]]、[[Configurable]]等
ES有两种属性:
- 数据属性
- 访问器属性
包含一个数据值的位置,可以读写。4个特性:
[[Configurable]] |
| 直接在对象上定义的属性,默认值true |
[[Enumerable]] | 能否通过for-in循环返回属性 | |
[[Writable]] | 能否修改属性的值 | |
[[Value]] | 包含这个属性的数据值 | undefined |
要修改属性默认的特性,需使用es5的
Object.defineProperty()
方法
三个参数:属性所在的对象,属性的名字(需是字符串),一个描述符对象
这里的name就是数据属性
var person = {};
Object.defineProperty(person, 'name', {
writable: false,
value: "Nicholas",
});
console.log(person.name); // Nicholas
person.name = "Greg";
console.log(person.name); // Nicholas
delete person.name;
console.log(person.name);
把configurable设置为false,表示不能从对象中删除属性。一旦把属性定义为不可配置的,就不能再把它变回可配置的。
在调用
Object.defineProperty()
创建一个新属性时,如果不指定,configurable、enumerable、writable特性的默认值都是false。
不包含数据值,访问器属性不能直接定义,要通过Object.defineProperty()这个方法来定义。
同上 | ||
[[Get]] | 读取属性时调用 | |
[[Set]] | 写入属性时调用 |
var book = {
_year: 2020,
edition: 1,
};
Object.defineProperty(book, "year", {
get: function() {
return this._year;
},
set: function(newVal) {
console.log(newVal > 2020);
if (newVal > 2020) {
this._year = newVal;
this.edition += newVal - 2020;
}
},
});
book.year = 2024;
console.log(book.edition);
_year表示内部属性,只能通过对象方法访问的属性
year为访问器属性,包含一个getter函数和一个setter函数,非必须同时存在。
定义多个属性
Object.defineProperties(book,{数据属性、访问器属性})
读取属性的特性
Object.getOwnPropertyDescriptor(book,"year")
获得给定属性的描述符。
两个参数:属性所在的对象,要读取其描述符的属性名称。
创建对象
Object构造函数、对象字面量
创建单个对象,缺点:大量重复代码
工厂模式
使用函数封装以特定接口创建对象的细节,即将公用部分提取到一个函数中,接收特定参数,返回创建的对象。
function createPerson(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;
}
缺点:存在对象识别问题,不知道一个对象的类型,因为都是Object类型。
构造函数模式
ES中的构造函数可以用来创建特定类型的对象。
首字母大写,以示区别于其他函数。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
}
}
var p1 = new Person("Nick", 29, "Engineer");
var p2 = new Person("Greg", 27, "Doctor");
不同之处:没有显示创建对象;直接将属性和方法赋给this对象;没有返回值;
要创建Person新实例,使用new操作符,4步:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象,this指向新对象;
- 执行构造函数中的代码,为新对象添加属性;
- 返回新对象;
p1、p2分别保存着Person的一个不同实例,这两个对象都有个constructor属性,该属性指向Person。
优势:创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
所有对象均继承自Object,函数是对象,即一个Object类型实例
构造函数,通过new操作符来调用才可以作为构造函数,不通过new操作符调用,和普通函数无异。
构造函数的问题:
- 每个方法都要在每个实例上重新创建一遍,不同实例上的同名函数是不相等的。
- 创建两个完成同样任务的Function实例是没有必要的。
- 根本不用在执行代码前就把函数绑定到特定对象上面,有this对象可以借助。
- 把函数转移到构造函数外部,变成可共享的全局作用域函数,但这个函数实际上只能被某些对象调用,名不副实。
- 如果存在很多函数,都定义在全局作用域中,这个自定义类型的构造函数就没有封装性可言。
原型模式
每个函数都有一个prototype属性,是一个指针,指向一个对象。
这个对象的用途:包含可以由特定类型的所有实例共享的属性和方法,可以理解为一个类功能存储器。
原型对象的好处:让所有对象实例共享它所包含的属性和方法。
不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
function Person(){}
Person.prototype.name = "Nick";
Person.prototype.age = 29;
Person.prototype.job = "Engineer";
Person.prototype.sayName = function(){
console.log(this.name);
}
var p1 = new Person();
p1.sayName();
var p2 = new Person();
p2.sayName();
console.log(p1.sayName == p2.sayName);
理解原型对象
创建一个新函数,根据特定规则,为该函数创建一个prototype属性,这个属性指向函数的原型对象。
原型对象自动获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。
Person.prototype.constructor = Person
原型对象默认只会取得constructor属性,至于其他方法,都是从Object继承而来。
调用构造函数创建一个新实例后,该实例内部将包含一个指针,指向构造函数的原型对象。[[Prototype]],非标准实现__proto__
原型对象F.prototype | 构造函数F | 实例f |
constructor | isPrototypeOf() | |
prototype | new | |
__proto__/Object.getPrototypeOf() | instanceof |
Person.prototype.isPrototypeOf(p1); // true
Person.prototype.isPrototypeOf(p2); // true
Object.getPrototypeOf(p1) == Person.prototype; // true
多个对象实例共享原型所保存的属性和方法的基本原理:搜索原型链。
原型链的最前端是当前对象实例,其次是创建该实例的构造函数的原型对象,依次类推,直到顶级Object.prototype。这也是原型链的查找路径。
在实例中添加一个属性,这个属性就会屏蔽原型对象中保存的同名属性,但是不会修改原型中的属性值。
hasOwnProperty()
可以检测一个属性是存在实例中,还是存在于原型中。只在给定属性存在于对象实例中才会返回true。来自原型则返回false。
通过使用hasOwnProperty()方法,什么时候访问的是实例属性,什么时候访问的是原型属性就一清二楚了。
console.log(p1.hasOwnProperty("name"));
p1.name = "kevin";
console.log(p1.hasOwnProperty("name"));
in操作符
通过对象能够访问给定属性时返回true,无论该属性在实例中还是原型中。
console.log("name" in p1); // true
判断属性是否为原型中属性
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性,存在于实例中,也存在于原型中的所有属性。所有开发人员定义的属性都是可枚举的。
Object.keys() —— 获取对象上所有可枚举的实例属性
Object.getOwnPropertyNames() —— 得到所有实例属性,无论是否可枚举
简化原型语法
function Person(){}
Person.prototype = {
name: '',
age:18,
job:"",
sayName: function(){}
}
这样写会改变两点:使用字面量创建新对象,constructor属性不再指向Person,而是指向Object构造函数。
还要小心重设constructor属性会导致它的[[Enumerable]]特性被设置为true,默认情况下,原生的constructor属性是不可枚举的。
constructor: Person,
使用Object.defineProperty()来设置:
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
})
把原型修改为另外一个对象就等于切断了构造函数于最初原型之间的联系。而实例中的指针仅指向原型,而不指向构造函数。改变原型对象,对于已存在的实例,仍指向旧的原型对象。
原型模式最大的问题:由其共享的本性导致的。
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式——构造函数定义实例属性,原型模式定义方法和共享属性。
每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度节省了内存。这种混成模式还支持向构造函数传递参数。
function Person(name,age,job){
this.name=name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name);
}
}
var p1 = new Person("Nick", 29, "Engineer");
var p2 = new Person("Greg", 27, "Doctor");
p1.friends.push("Van");
console.log(p1.friends);
console.log(p2.friends);
console.log(p1.friends === p2.friends);
console.log(p1.sayName === p2.sayName);
动态原型模式
function Person(name,age,job){
this.name=name;
this.age = age;
this.job = job;
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
console.log(this.name);
}
}
}
var friend = new Person("Nick", 29, "Engineer");
friend.sayName();
if语句只会在初次调用构造函数时才会执行。
使用动态原型模式,不能使用对象字面量重写原型,会切断现有实例与新原型之间的联系。
寄生构造模式
function SpecialArray(){
var values = new Array();
values.push.apply(values, arguments);
values.toPipedString = function(){
return this.join("|");
}
return values;
}
var colors = new SpecialArray("red","blue","green");
console.log(colors.toPipedString()); // red|blue|green
返回的对象与构造函数或者与构造函数的原型属性之间没有关系,不能依赖instanceof操作符确定对象类型。
稳妥构造函数模式
function Person(name, age, job){
var o = new Object();
//此处定义私有变量和函数
// 添加方法
o.sayName = function(){
console.log(name);
};
return o;
}
var friend = Person("Nick", 29, "Engineer");
friend.sayName();
稳妥对象没有公共属性,而且其方法也不引用this的对象。不使用new操作符调用构造函数。
继承
ES只支持实现继承,实现继承继承实际的方法;不支持接口继承,因为接口继承只继承方法签名,而ES函数没有签名。
实现继承依靠原型链来实现。
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
让原型对象等于另一个类型的实例。
SubType.prototype = new SuperType();
给原型添加方法的代码一定要放在替换原型的语句之后。
通过原型链实现继承时,不能使用对象字面量创建原型方法,会重写原型链。
原型链存在的问题
所有子类实例都会共享一个父类属性;不能向超类型的构造函数中传递参数。
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);
var instance2 = new SubType();
console.log(instance2.colors);
借用构造函数
在子类型构造函数的内部调用超类型构造函数。
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
// 调用超类构造方法
SuperType.call(this);
}
// SubType.prototype = new SuperType();
var instance1 = new SubType();
// 每个实例都会有超类的属性副本
instance1.colors.push("black");
console.log(instance1.colors);
var instance2 = new SubType();
console.log(instance2.colors);
可以传递参数
fucntion SuperType(name){
this.name = name;
}
function SubType(){
SuperType.call(this, 'Nick');
this.age = 18;
}
var instance = new SubType();
console.log(instance); // SubType {name: "Nick", age: 18}
问题:函数复用的问题,方法都在构造函数中定义。超类型原型中定义的方法,对子类型而言也是不可见。很少单独使用。
组合继承——最常用
将原型链和借用构造函数的技术组合到一块,发挥二者之长的模式。
使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
问题:无论什么情况下,都会调用两次超类型构造函数——创建子类型原型时、子类型构造函数内部。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType();
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new SubType('Nick', 29);
instance1.colors.push("black");
instance1.sayName();
instance1.sayAge();
var instance2 = new SubType('Greg', 27);
instance2.sayName();
instance2.sayAge();
原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o){
function F(){};
F.prototype = o;
return new F
}
ES5新增Object.create()方法规范化了原型式继承。接收两个参数:一个用作新对象原型的对象(模版对象),一个新对象定义额外属性的对象。
var person = {
name: "Nick",
friends: ["","",""]
}
var another = Object.create(person, {
name: {
value: "Greg",
}
})
寄生式继承
创建一个仅用于封装继承过程的函数,在内部增强对象。
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log("hi");
}
return clone;
}
寄生组合式继承——引用类型最理想的继承范式
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
本质:使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
// SubType.prototype = new SuperType();
// SubType.prototype.constructor = SubType();
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new SubType('Nick', 29);
instance1.colors.push("black");
instance1.sayName();
instance1.sayAge();
var instance2 = new SubType('Greg', 27);
instance2.sayName();
instance2.sayAge();
7. 函数表达式
在 JavaScript 编程中,函数表达式是一种非常有用的技术。使用函数表达式可以无须对函数命名,从而实现动态编程。匿名函数,也称为拉姆达函数,是一种使用 JavaScript 函数的强大方式。以下总结了函数表达式的特点。
函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫做匿名函数。
在无法确定如何引用函数的情况下,递归函数就会变得比较复杂;
递归函数应该始终使用 arguments.callee 来递归地调用自身,不要使用函数名——函数名可能会发生变化。
当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量,原理如下。
在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。
但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。
使用闭包可以在 JavaScript 中模仿块级作用域(JavaScript 本身没有块级作用域的概念),要点如下。
创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。
闭包还可以用于在对象中创建私有变量,相关概念和要点如下。
即使 JavaScript 中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。
有权访问私有变量的公有方法叫做特权方法。
可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。
JavaScript 中的函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。不过,因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。
定义函数的两种方式:(理解其区别)
函数声明,函数声明提升,可以把函数声明放在调用语句之后。
函数表达式:创建一个函数并将它负值给变量,匿名函数=拉姆达函数,不存在提升。
递归
在一个函数通过名字调用自身的情况下构成的。
arguments.callee() 是一个指向正在执行的函数的指针
// 严格模式报错
function factorial(num) {
if (num <= 1) {
return num;
} else {
return num * factorial(num - 1);
}
}
// 通用
var fact = function f(num) {
if (num <= 1) {
return num;
} else {
return num * f(num - 1);
}
};
闭包
闭包是指有权访问另一个函数作用域中变量的函数。
常见形式:函数内部创建函数,并返回这个内部函数。
当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。
然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。
function compare(value1, value2){
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
先定义了 compare()函数,然后又在全局作用域中调用了它。当调用 compare()时,会创建一个包含 arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 result 和 compare)在 compare()执行环境的作用域链中则处于第二位。
后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像 compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建 compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用 compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。对于这个例子中 compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在 createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数 createComparisonFunction()的活动对象。图 7-2 展示了当下列代码执行时,包含函数与内部匿名函数的作用域链。
function createComparisonFunction(propertyName) {
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}
var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });
在匿名函数从 createComparisonFunction()中被返回后,它的作用域链被初始化为包含
createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在
createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当 createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁,例如:
//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
//解除对匿名函数的引用(以便释放内存)
compareNames = null;
首先,创建的比较函数被保存在变量compareNames 中。而通过将compareNames 设置为等于null解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全地销毁了。下图展示了调用 compareNames()的过程中产生的作用域链之间的关系。
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭包。虽然像 V8 等优化后的 JavaScript 引擎会尝试回收被闭包占用的内存,但请大家还是要慎重使用闭包。
闭包与变量
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
var result = createFunctions();
result[0]();
每个函数都返回 10。因为每个函数的作用域链中都保存着 createFunctions() 函数的活动对象,所以它们引用的都是同一个变量 i 。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
var result = createFunctions();
result[0]();
在调用每个匿名函数时,我们传入了变量 i。由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数 num。而在这个匿名函数内部,又创建并返回了一个访问 num 的闭包。这样一来,result 数组中的每个函数都有自己num 变量的一个副本,因此就可以返回各自不同的数值了。
this对象
this 对象是在运行时基于函数的执行环境绑定的。
匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。
每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //"My Object"
var that = this;
在定义匿名函数之前,我们把 this 对象赋值给了一个名叫 that 的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声名的一个变量。
内存泄漏
function assignHandler(){
// 这是一个闭包
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对 assignHandler()的活动对象的引用,因此就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所占用的内存就永远不会被回收。
function assignHandler(){
var element = document.getElementById("someElement");
// 用变量保存,消除循环引用
var id = element.id;
element.onclick = function(){
alert(id);
};
// 活动对象不消失,依旧存在引用,手动置null解除联系
element = null;
}
解决方法:
- 通过把 element.id 的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。
- 即使闭包不直接引用 element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把 element 变量设置为 null。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。
必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着 element。
JavaScript 从来不会告诉你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。匿名函数可以用来模仿块级作用域并避免这个问题。
(function(){
//这里是块级作用域
})();
等价于
var someFunction = function(){
//这里是块级作用域
};
someFunction();
JavaScript 将 function 关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。
然而,函数表达式的后面可以跟圆括号。
要将函数声明转换成函数表达式,只要像下面这样给它加上一对圆括号即可。
很多库都有这种用法
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。在一个由很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。
(function(){
var now = new Date();
if (now.getMonth() == 0 && now.getDate() == 1){
alert("Happy new year!");
}
})();
这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
私有变量
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
function add(num1, num2){
var sum = num1 + num2;
return sum;
}
如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。我们把有权访问私有变量和私有函数的公有方法称为特权方法。
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据。
第一种是在构造函数中定义特权方法,不过构造函数模式的缺点是针对每个实例都会创建同样一组新方法
function MyObject(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法,这就是一种闭包
this.publicMethod = function (){
privateVariable++;
return privateFunction();
};
}
这个模式在构造函数内部定义了所有私有变量和函数。然后,又继续创建了能够访问这些私有成员的特权方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。
function Person(name){
this.getName = function(){
return name;
};
this.setName = function (value) {
name = value;
};
}
var person = new Person("Nicholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"
静态私有变量
第二种,通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法
(function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数
MyObject = function(){
};
//公有/特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
};
})();
需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没有在声明 MyObject 时使用 var 关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject 就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,在严格模式下给未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视你的具体需求而定。
多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个显明的不足之处。
模块模式
前面的模式是用于为自定义类型创建私有变量和特权方法的。
道格拉斯所说的模块模式(module pattern)则是为单例创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。按照惯例,JavaScript 是以对象字面量的方式来创建单例对象的。
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权/公有方法和属性
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
};
}();
这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的
var application = function(){
//私有变量和函数
var components = new Array();
//初始化
components.push(new BaseComponent());
//公共
return {
getComponentCount : function(){
return components.length;
},
registerComponent : function(component){
if (typeof component == "object"){
components.push(component);
}
}
};
}();
在 Web 应用程序中,经常需要使用一个单例来管理应用程序级的信息。
简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。
增强的模块模式
改进的模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//创建对象
var object = new CustomType();
//添加特权/公有属性和方法
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
//返回这个对象
return object;
}();