不是我写的,从 @gtg师傅那偷的(原文链接),佬!!! Orz已征求师傅许可(不许可也不行 狗头狗头)
原型链prototype chain
没有类的实例对象
Javascript继承机制的设计思想): Brendan Eich设计 Javasciprt 的初衷只是为了浏览器与网页互动,因而主要目标就是高效。
当时的热门编程语言Java
和C++
都使用new
命令生成实例,如果要一个 class 要继承另一个,就直接用相应的语法继承就行了。
BE在 Javascript 中学习了new
的命令方法,但考虑到JS的简洁易用性,他不想要引入”类”的概念,转而使用”类的构造函数”(constructor)替代。
比如创建对象的时候,C++的写法是:
ClassName *object = new ClassName(param);
Java的写法是:
Foo foo = new Foo();
而Javascript:
function DOG(name){
this.name = name; //this指向新创建的实例对象,在多层访问仍然指向最初的实例
}
var dogA = new DOG('大黄')
可以看到 JS 中并没有声明类( Java 和 C++ 中笔者没写出来),而是在创建实例的时候直接执行定义的DOG()
构造函数。这样看来语法就变得简洁很多了。
原型链解决共享属性问题
上文用构造函数创建的每一个实例对象,都有自己的属性和方法的副本,不受其他对象的属性更改影响。这样不仅无法做到数据共享,也是极大的资源浪费。
考虑到这一点,BE决定为构造函数设定一个prototype
属性。
这个prototype
属性包含一个对象,所有需要共享的属性都放在这个对象中,不需要共享的属性则各自通过构造函数定义或直接定义。
比如:
function DOG(name){
this.name = name;
}
DOG.prototype = { species : '犬科' };
var dogA = new DOG('大毛');
var dogB = new DOG('二毛');
alert(dogA.species); // 犬科
alert(dogB.species); // 犬科
这样一来就可以轻易地修改来自同一”构造函数”构造的实例对象的属性了。
原型访问过程
既然prototype
是赋予构造函数的原型,那在对象中是如何访问原型属性的呢?
这是通过对象的__proto__
属性实现的。比如上面的 dogA 对象,他的__proto__
指向DOG.prototype
。当你访问dogA.species
时,发现dogA
并不存在species
属性,这时就会访问dogA.__proto__.species
,也就是DOG.prototype.species
,发现这个属性是存在的,所以就返回了原型的属性值”犬科”。当然如果存在多级原型关系的话,同样会去寻找__proto__.__proto__.__proto__...
。这样的靠__proto__
串起来的逐级访问过程就是原型链。
而实际上,在JS中任何一个对象都具有__proto__
属性,这一属性指向的是底层对象Object.prototype
。当构造函数的prototype
被指向其他对象时,你仍然可以通过dogA.__proto__.__proto__
去访问底层Object.prototype
,这也是原型链污染经常利用的点。
方法的原型关系与属性相同,例子如下:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}
var nick = new Person('nick', 18);
console.log(nick.__proto__ === Person.prototype) // true
nick.log(); //输出nick, age:18
nick.__proto__.log(); //undefined, age:undefined,因为this指向新创建的实例对象,在多层访问仍然指向最初的实例
这种设计使得 Javescript 语法简洁了许多,既满足了共享属性的需求,又可以通过实例的各自定义满足独立属性或方法的需求。
需要提及的是赋值操作不会自动去访问原型,而是创建该实例的独立属性或方法,不会自动覆盖原型的属性或方法
总结一下
更多细节参见:該來理解 JavaScript 的原型鍊了 – Huli
注:
class
关键字以及一系列与类相关的方法在后来的ES6标准
中出现,但仍然保留了原型关系特性(有些人认为这个类只是原型链的语法糖
作用域与闭包
作用域Scope
作用域就是一个变量的有效范围。哪里呼叫这个参数能指向他的存储地址的,都是其作用域。
在 Javascript 中,每一个 function
进入之前都会有一个 Execution Contexts
执行环境,存放了执行需要的所有信息。这个 Execution Contexts
又带有一个与之对应的 variable object
,在这个对象中存放需要的所有变量。
如果在某一个 function
中呼叫 a
变量,解释器就会从最小的、即自身的 variable object
中寻找 a
变量,然后逐级向上访问,直到访问到 a
的值,或访问到 Global Scope
没有发现而抛出错误为止。这种从 Global Scope
到一层层 fuction
就构成了整体的 scope chain
。
举个例子
var a = 1
function echo() {
console.log(a) // 1 or 2?
}
function test() {
var a = 2
echo()
}
test()
在这样一段代码中, echo()
函数执行之前似乎又有了 a
变量的重新定义。但实际上在 js 作用域只讲层级不讲先后, test()
中定义的 a
并不会对 echo()
的执行环境造成任何影响。所以这里的输出结果是 1。
这种特性叫做lexical scope
静态作用域,相对应的自然还有 dynamic scope
动态作用域
比如下面这两段
var b = 2;
function demo(str, a) {
eval(str);
console.log(a, b);
}
demo("var b = 12;", 1); // 1, 12
JS真费脑筋。。动态作用域具体可以参考这篇 你不知道的Javascript动态作用域 – 掘金 (juejin.cn)
大概就是使用特殊的方法eval
和with
强行修改作用域,造成”欺骗”。
闭包Closure
先看这段代码
function test() {
var a = 10
function inner() {
console.log(a)
}
return inner
}
var inner = test()
inner() //10
将内部函数inner()
作为返回值传出赋值给外部函数,执行后发现仍能获取test()
内部定义的a
变量。
这意味着原有test()
函数在执行后没有回收而是继续占用内存。只要函数inner()
存在,test()
函数相关的资源(包括test和a的定义)都不会被释放。这种看似执行完毕却还有东西没有释放的,就叫做Closure
闭包。
那么闭包的优点是什么?
function getWallet() {
var my_balance = 999
return {
deduct: function(n) {
my_balance -= (n > 10 ? 10 : n) // 超過 10 塊只扣 10 塊
process.stdout.write(mybalance.toString()+'\n')
}
}
}
var wallet = getWallet()
wallet.deduct(13) // 989
wallet.deduct(15) // 979
my_balance -= 999 // Uncaught ReferenceError: my_balance is not defined
闭包这种写法能保证内部变量my_balance
仅允许限定的方法进行修改,并且由于内存始终没有释放,内部变量也可以记录状态。
闭包还能够解决下面这样的问题
var btn = document.querySelectorAll('button') //获取所有button
for(var i=0; i<=4; i++) {
btn[i].addEventListener('click', function() {
alert(i) //5 5 5 5 5
})
}
代码原意是想让每一个按钮弹出不同的数字1,2,3,4,但实际上只会弹出5。这是因为监听器内的函数仅仅是得到了定义而没有执行。而对参数i
的调用无法再内部的作用域找到,只能在全局作用域中找到一个已经经历循环的i
,因而每一个按钮都会弹出5。
如果采用闭包的方法改成下面这样
function getAlert(num) {
return function() {
alert(num)
}
}
for(var i=0; i<=4; i++) {
btn[i].addEventListener('click', getAlert(i))
}
循环的过程中调用 getAlert()
返回的function
不再依附于i
,生成了独立的五个function
就能实现预期的功能。还可以使用Immediately-Invoked Function Expression
即调函数表达式的方法,这里就不再记录了。
但实际上这种写法还是有点麻烦,幸好ES6
加入了一个叫做block scope
块级作用域的东西。
块级作用域就是仅在一定范围内的生效的作用域,外部无法访问。function
的{}
包裹内容实际上也是块
,但一般不称作块级作用域
。
仅仅像下面这样将var
修改为let
就能解决上面的问题
for(let i=0; i<=4; i++) {
btn[i].addEventListener('click', function() {
alert(i) // 0 1 2 3 4
})
}
let
的声明方法会为每一个function
创造一个针对他的块级作用域,使得上述代码可以理解为这样
{
let i=0
btn[i].addEventListener('click', function() {
alert(i)
})
}
{
let i=1
btn[i].addEventListener('click', function() {
alert(i)
})
}
...
(看起来并不是很高效
function的建立和执行过程
一、建立全局执行环境,获取其中被定义的所有对象,进行声明(没有赋值),并记录其中哪个是函数
二、执行函数外部的代码,获取全局变量组为一个对象,即`activation object`
三、进入函数,获取函数传递参数与全局变量组结合为一个变量组,即`variable object`,并构造作用域链
四、执行函数内部代码
如果說你認為閉包一定要:「離開創造它的環境」,那顯然「所有的函式都是閉包」這句話就不成立;但如果你認同閉包的定義是:「由函式和與其相關的參照環境組合而成的實體」,那就代表在 JavaScript 裡面,所有的函式都是閉包。
嘶~~
本来还打算做一点参数传递、变量提升的内容总结,但是我能力有限删删改改千来字,发现总结不了((🌿
参见 huli JavaScript 五講,非常精彩,文章大部分的参考也来自这里。