JS 特性

不是我写的,从 @gtg师傅那偷的(原文链接),佬!!! Orz
已征求师傅许可(不许可也不行 狗头狗头)

原型链prototype chain

没有类的实例对象

Javascript继承机制的设计思想): Brendan Eich设计 Javasciprt 的初衷只是为了浏览器与网页互动,因而主要目标就是高效。

当时的热门编程语言JavaC++都使用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)

大概就是使用特殊的方法evalwith强行修改作用域,造成”欺骗”。

闭包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 五講,非常精彩,文章大部分的参考也来自这里。

许可证明😜

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇