前言
ps: 2018/05/13 经指正之后发现惰性加载函数细节有问题,已改正
在这里也补充一下,这些都是根据自己理解写的例子,不一定说的都对,有些只能查看不能运行的要谨慎,因为我可能只是将方法思路写出来,没有实际跑过的.
面向对象编程 && 面向过程编程
面向对象编程(Object Oriented Programming,OOP)
是一种以事物为中心的编程思想,把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为,三大特点缺一不可。
特点 | 作用 |
---|---|
封装 | 将其说明(用户可见的外部接口)与实现(用户不可见的内部实现)显式地分开,其内部实现按其具体定义的作用域提供保护 |
继承 | 子类自动共享父类数据结构和方法的机制 |
多态 | 相同的操作或函数、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果 |
面向过程编程(Procedure Oriented Programming, POP)
是一种以过程为中心的编程思想,分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
百度百科有个很形象的比喻,
例如五子棋:
面向过程的设计思路步骤:
1、开始游戏,
2、黑子先走,
3、绘制画面,
4、判断输赢,
5、轮到白子,
6、绘制画面,
7、判断输赢,
8、返回步骤2,
9、输出最后结果面向对象的设计思路步骤:整个五子棋可以分为
1、黑白双方,这两方的行为是一模一样的,
2、棋盘系统,负责绘制画面,
3、规则系统,负责判定诸如犯规、输赢等。
第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了多个步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。
(更多内容请自行查阅,本节到此为止了.)
基本类型和引用类型
之前已经写过这个文章,就不复述了
详情可以参考我之前写的文章关於Javascript基本类型和引用类型小知识
执行环境(execution context)及作用域(scope)
来自Javascript高级程序设计3:
解析
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object)
,环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。全局执行环境是最外围的一个执行环境。根据 ECMAScript 实现所在的宿主环境不同,表示执行环境的对象也不一样。在 Web 浏览器中,全局执行环境被认为是
window 对象
,因此所有全局变量和函数都是作为 window (全局)对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)。每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。 ECMAScript 程序中的执行流正是由这个方便的机制控制着。
当代码在一个环境中执行时,会创建变量对象的一个
作用域链(scope chain)
。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)
作为变量对象。活动对象在最开始时只包含一个变量,即arguments 对象
(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生).
延长作用域链
- try-catch 语句的 catch 块
- with 语句(不推荐)
这两个语句都会在作用域链的前端添加一个变量对象。
对 with 语句来说,会将指定的对象添加到作用域链中。
对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
没有块级作用域
function test() {
if (true) {
var num = 0;
}
//打印if语句内声明变量
console.log(num);//0
for (var i = 0; i < 4; i++) { }
//打印for语句内声明变量
console.log(i);//4
}
test()//0 4
在其他类 C 的语言中,由花括号封闭的代码块都有自己的作用域,在if ,for语句执行完毕后被销毁,但在 JavaScript 中, if ,for语句中的变量声明会将变量添加到当前的执行环境中。如果向模拟块状作用域的话可以利用闭包等方法,下文会提到.
(更多内容请自行查阅,本节到此为止了.)
垃圾收集
JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。
下面我们来分析一下函数中局部变量的正常生命周期。
局部变量只在函数执行的过程中存在。而在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值。然后在函数中使用这些变量,直至函数执行结束。此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用。在这种情况下,很容易判断变量是否还有存在的必要;
但并非所有情况下都这么容易就能得出结论。垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略。
- 标记清除
当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。 - 引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
问题一, 循环引用会一直无法回收;
问题二, 低版本IE有一部分对象并不是原生 JavaScript 对象;
(更多内容请自行查阅,本节到此为止了.)
JavaScript原型对象与原型链
之前已经写过这个文章,就不复述了,特意修改了之前的排版补充之类
詳情可以參考我之前寫的文章關於Javascript中的new運算符,構造函數與原型鏈一些理解
递归
一种会在函数内部重复调用自身的写法.
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
console.log(factorial(5));//120
一开始的常规写法,但是有个问题是内部调用自身是使用函数名字,如果在将factorial赋值到一个变量之后,尽管还是调用原factorial函数,但不是期望的调用函数自身的写法了.
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
var another = factorial;
factorial = null;
console.log(another(5)); //TypeError: factorial is not a function
如上,实际上是在another上调用factorial,而且如果factorial不存在之后会引起错误.
解决方案:
1, arguments.callee(不推荐)
是一个指向正在执行的函数的指针属性.
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
var another = factorial;
factorial = null;
console.log(another(5)); // 120
缺点:
- 严格模式下,不能通过脚本访问
arguments.callee
,访问这个属性会导致错误; - arguments是庞大且变化的,每次访问需要消耗大量性能;
2, 命名函数表达式
var factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
});
var another = factorial;
factorial = null;
console.log(another(5)); // 120
这种方式在严格模式和非严格模式下都行得通.
(更多内容请自行查阅,本节到此为止了.)
闭包
定义:
- 未知来源: 指函数变量可以保存在函数作用域内,因此看起来是函数将变量“包裹”了起来;
- 未知来源: 指在函数声明时的作用域以外的地方被调用的函数;
- 官方: 一个拥有许多变量和绑定了这些变量的环境的表达式;
- Javascript高级程序设计3: 闭包是指有权访问其他函数作用域中的变量的函数;
- JavaScript语言精粹: JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里;
- 阮一峰: 闭包就是能够读取其他函数内部变量的函数;
这是一个很难界定的点,每个人的说法都不同,包括各种专业资料,权威大神,但是唯一不变的是它们都有提到访问其他作用域的能力.
例如这个例子,不仅可以在外部读取函数内部变量,还能修改.
function test() {
var num = 1;
return {
get: function () {
console.log(num);
},
add: function () {
console.log(++num);
}
}
}
var result = test();
result.get(); // 1
result.add(); // 2
注意:
1, 匿名函数和闭包函数没有必然关系;
匿名函数: 不需要函数名字,没污染全局命名空间的风险并且执行后自动销毁环境.
很多人说匿名函数也是闭包的用法,但是在我看来这只不过是使用匿名函数的写法来写闭包,让开发省掉多余步骤而已.例如:
//闭包写法
function test1() {
return function () {
console.log(1);
}
}
test1()(); // 1
//匿名函数写法
var test2 = (function () {
return function () {
console.log(1);
}
})()
test2(); // 1
2, 闭包所保存的是整个变量对象,而不是某个特殊的变量,所以只能取得包含函数中任何变量的最后一个值。
这就是为什么for循环返回的i永远是最后一个的原因了
- 点我吧!!
- 点我吧!!
- 点我吧!!
- 点我吧!!