Wierd Parts
Motavition
一直以来JavaScript是我认为设计的比较奇怪的一门语言,一个原因是JavaScript中一切皆为Object,导致很多概念之间是模糊的,比如函数,对象,Prototype,Interface等等。而且它和同时期主流的编程语言设计有着非常大的不同,一个最重要的差别是,JavaScript采用基于Prototype的继承方式,而不是类继承,但是它的语法又借鉴了其它面向对象语言(比如引入了关键字new
,this
等,后面我们会看到这实际上是一个很糟糕的设计),这使得很多C++或者Java的程序员误以为JavaScript的面向对象和C++或Java是相同的。实际上,JavaScript完全可以采用一套自己独有的语言设计风格,这种基于Prototype + 弱类型设计,可以使其足够的灵活,也足以衍生出许多比C++或Java更富表现力的设计。
Scope
JavaScript中的作用域设计的似乎有一些反常识,比如下面代码:
var a = 1;
//create a scope
{
var a = 2;
}
console.log(a); //2
//function scope
function func(){
var a = 3;
}
console.log(a); //2
上述代码中,第一个{}
中的a
改变了全局变量a
的值,说明JS中单独的{}
并不产生作用域的效果,但是函数body的{}
却可以。
在随后的ES6中似乎修正了这个问题,
let
关键字可以保证变量严格受scope约束
为了进一步了解作用域的问题,下面我们分析一下var a = 2
的执行过程:
-
当编译器遇到这行代码时,首先查看
a
所在的作用域中是否有该符号,如果有则忽略该语句,继续向下执行,否则会向作用域注册一个a
符号并赋值undefined
。上面的例子中,由于{}
不产生作用域,var a = 2
中的a
与{}
外层的a
共享作用域,因此这条语句会被编译器忽略。 -
接下来,编译器在编译完上述代码后,会生成可执行代码和运行时上下文,当执行到
var a = 2
时,JS的引擎会先从运行时上下文中查找a
是否存在,如果存在,则将其指向的内容变为2
,不存在则报错。
按照上面的过程,我们就不难解释JavaScript中所谓的”Hoisting”:
console.log(a) //undefined
var a = 10
当执行console.log(a)
时,编译器已经将a
注册到运行时的上下文中了,并赋予了初值undefined
,因此console.log(a)
输出undefined
。接下来引擎会
Function
函数可以说是JavaScript中最出彩的设计,如果放到在当时的年代,这种设计确实很大胆也很领先。在JS中的函数是First-Class Object,它即有函数的特性,也同时具有Object的特性,这使得在JS中函数的定义和使用非常灵活,
function func(name){
console.log(name)
}
函数名func
也是函数对象的名称,可以像使用任何Object一样使用func
,比如可以给它增加成员函数,成员变量,做参数传递,做返回值返回等等。
func.index = 1;
func.log = function(text){
console.log(text)
}
func.log("some text") //some text
和Object相同的是,它也有自己的所谓的”基类”,即Function.prototype
,而Function.prototype
也是一个Object,它的”基类“是Object.prototype
:
func.__proto__ === Function.prototype; //true
Function.prototype.__proto__ === Object.prototype //true
上述代码可看出,Function
和Object
均为大写开头,这种规则“暗示”其类型为一个”构造函数“。”构造函数”也是一种函数对象,而函数对象和普通对象不同的是,它还有一个prototype
属性:
func.prototype; //{constructor: ƒ,__proto__:Object}
func.prototype
的类型也是Object,它只有一个成员叫做constructor
,而construtor
的值又指向func
。
这是一个看起来特别让人困惑的设计,为什么要给函数对象增加这么一个对象? 实际上引入这个prototype
是为了模拟”继承“或者Interface,实现代码的重用。由于JavaScript中没有提供一种专门的”构造函数“,因此每个函数都会得到一个prototype
对象。而constructor
属性目前看来还没有什么用。
在后面讨论JavaScript面向对象的时候还会继续深入讨论
prototype
机制。
最后说一下函数的传参问题,在JS中,没有所谓的函数签名,因此函数的声明和函数的调用也不需要match,我们可以不给函数声明任何参数,却可以在调用它的时候传递若干个参数。
function func(){
console.log(arguments[0]) //1
}
func(1,2,3)
this
this
在JS中是另一个古怪的存在,由于这门语言没有面向对象设计,因此this
应该无从谈起,但实际上this
却到处都有,有global的this
,函数中有this
,Object中也有this
。
Global Object
全局this
指向global object
console.log(this);
//Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
this.var = 100;
console.log(windown.var); //100
全局函数中的this
同样指向window obejct
function a(){
consloe.log(this); //points to window obejct
this.var = "hello"; //add var to window object
}
var b = function b(){
consloe.log(this); //window obejct
}
this
in Obejct
如果this
位于某个Object的内部,则this
指向该Obejct
var person = {
firstName: "Elie",
log:function(){
console.log(this) //points to person obejct
}
}
但是如果你认为person
内部定义的function其this
都指向person
的话,那么你就错了,如果在log
函数中定义另一个function,那么这个function中的this
指向的是global
object
var person = {
firstName: "Elie",
log:function(){
console.log(this) //person
var setname = function(name){
this.firstName = name; //this points to window object
}
setname("Tom");//not working
}
}
正确的做法是显式的定义一个this
的引用
log:function(){
var self = this;
var setname = function(name){
self.firstName = name;
}
setname("Tom");
}
bind
,apply
,call
var logName = function(str1, str2){
console.log(this.getFullName()) //window object
}
上述代码中,logName
是一个全局函数,由前面的讨论可知,此时this
指向global object,也就是window
,由于windown
中没有getFullName
这个方法,因此上述代码执行会出错。针对上面的情况,在JavaScript中,this
的值是可以改变的,我们可以让this
指向一个有getFullName
方法的对象:
var person = {
firstname: 'John',
lastname: 'Doe',
getFullName: function(){
var fullname = this.firstname + ' ' + this.lastname;
return fullname;
}
}
var logName = (function(str1,str2){
console.log(this.getFullName()) //wrong
}).bind(person)
logName() //John Doe
logName.call(person,"str1","str2") //John Doe
logName.apply(person,["str1,str2"])//John Doe
上述代码中令logName
函数中的this
指向了person
。
bind,call,apply
这三种方式均可以改变this
的指向,不同的是bind
并不执行函数,只是改变this
的值,call
,apply
会直接执行函数,call
和apply
的区别仅在传参的写法上。
凡是有this
的函数均可以用这几种方式去改变this
,比如
var person2 = {
firstname="Jane",
lastname="Doe"
}
person.getFullName.apply(person2)
bind
还以用来改变函数的行为
function map(arr, fn){
var ret = []
for(let item of arr){
ret.push(fn(item))
}
return ret;
}
const arr = map([1,2,3],function(limit,item){
return item > limit;
}.bind(this,1)) //limit的值为1
上面代码中,map
接受两个参数,一个是数组,另一个是fn
函数。fn
函数按照上面定义,接受一个数组元素作为参数。接下来当我们在调用map
的时候,却给fn
传了两个参数,一个是this
用作占位(对于fn
来说,这个this
没有实际意义)另一个是数字1
,此时对于fn
来说,传入的1
会被自动绑定到limit
上,而fn
此时也等价于下面的函数:
function(item){
limit = 1;
return item > 1;
}
IIFEs
第一次看到IIFEs这种形式的JS代码,完全不知道它是干嘛的,这种写法实际上隐含了JavaScript解释器的一些规则,看下面代码
var greeting = function(name){
return 'Hello' + name;
}('John')
console.log(greeting);
这时greeting
的类型是什么呢?string还是function? 显然上述写法中,greeting
变成了string
。不难理解上述代码的执行顺序为:
- 匿名函数求值
- 将求值结果保存在
greeting
中。
为什么会这么解释,在任何编程语言中,statement和expression是两个概念,statement是普通的语句,可以是条件语句,或者定义一个函数等等,而expression则表示一个表达式,而表达式是要立刻求值的。上面代码中,当编译器看到var greeting =
时,知道后面是一个表达式,进而对后面的匿名函数进行求值。而我们如果让编译器先看到function
,结果会是什么样呢?
function(name){ //wrong
return 'Hello ' + name;
}
错误原因是上面的语句以function
开头,编译器会认为它是一个函数定义的statement,而statement一定要有”主语”,即函数名,这样编译器才能将该符号注册到context中,由于上面语句没有函数名,因此报错。但是如果使用()
将其括起来,则语义就会发生变化:
(function(name){
return 'Hello ' + name;
})
在JS中()
包裹的statement为一个表达式expression,因此上述代码会被当做expression进行求值,显然求值的结果是得到了一个匿名函数(值得注意的是,匿名函数并没有求值)。因此该表达式的返回值是一个匿名函数,相当于
var func = (function(name){
return 'Hello ' + name;
})
如果想要执行这个匿名函数,则需要显式调用它
//#1
(function(name){
return 'Hello ' + name;
})("John")
//#2
(function(name){
return 'Hello ' + name;
}("John"))
这样,上述代码就变成了两个表达式。如果是第一种写法,则第一个()
表达式返回了一个匿名函数,第二表达式是匿名函数调用得到一个string;如果是第二种写法,则第一个表达式为函数求值,返回一个string,第二个表达式为()
什么也没做,直接返回该string。
这两种写法的结果一样,但是表达式的执行顺序却不同。
这种方式对于隔离全局变量很有帮助,由于JS没有命名空间,函数外定义的的变量都是全局的,如果我们想要执行一段代码又不污染全局变量,使用IIFE是一种好的方式
Closures
function greet(str1){
return function(str2){
console.log(str1+' '+str2)
}
}
var func2 = greet("Hi")
func2("Tony") //Hi Tony
上面代码中,按照常理理解,在执行func2
之前,greet
函数已经执行完了,str1
应该已经被释放了,为什么在执行func2
的时候还能访问到str1
呢?如果熟悉其它编程语言的Closure,比如C++的Lambda表达式,Objective-C的Block等,那么这个问题就不难理解。从现象看,是str1
被str2
所在的匿名函数给capture了,至于是怎么capture的,无外乎两种策略,拷贝或者引用,按照前面小节的推断,如果str1
是Primary Type,那么应该是拷贝,如果是Object类型,那么应该是传引用。但是对于Closures,无论任何数据类型,均是传引用,下面是一个经典的例子:
function buildFunctions(){
var arr = [];
for(var i =0; i<3;i++){
arr.push(function(){
console.log(i);
})
}
return arr;
}
var fs = buildFunctions();
fs[0](); //3
fs[1](); //3
fs[2](); //3
通过这个例子可以看出,即使是i
作为int型的Primary Type,对于Closure而言,保存的仍是它的引用。Closure这个特性可用来做异步任务,异步任务的回调函数可以capture在执行任务前的变量,比如
function timer_func(){
var greeting = "Hi!";
setTimeout(function(){
console.log(greeting);
},3000);
}
timer_func()
上述代码中,在执行3s后输出Hi!
,其原因是setTimer
的回调函数capture了greeting
。JavaScript的这个特性对很多Framework的设计起到了非常关键的作用。
Call By Value / Call by Reference
这个问题是理解每一门编程语言都要绕不过去的问题,本质上是内存分配问题,无论是Python,C++,Java等等,比如C++中函数传参可以有传值和传引用两种方式,所谓传值就是拷贝,传引用就是传地址。理解这个问题是正确处理Side Effect的基础。
在JS中对Primary Type类型的对象传值,对Object类型的对象传引用。即是不是传参的情况,JS对象之间的赋值也是引用的传递。
//pass by value
function change(b) { b = 2; }
var a = 1;
change(a);
console.log(a) // still 1
//pass object by reference
function changeObj(d){
d.prop1 = "string"
}
var c = {}
c.prop1 = {};
changeObj(c);
consloe.log(c.prop1); //string
null & undefined
JavaScript中的null
表示显式的指明某变量的值为空或者0,undefined
表示”无”的原始值,转为数值时为NaN。
var x;
console.log(x); //undefined
var y = null;
console.log(y); //null
Number(null); //0
5 + null //5
5 + undefined //NaN
在应用上二者没有本质的区别,在条件判断中都是false
。
if (!undefined)
console.log('undefined is false');// undefined is false
if (!null)
console.log('null is false');// null is false
undefined == null // true
对于undefined
,可以用在
- 变量被声明了,默认值为undefined。
- 调用函数时,缺省参数默认为undefined。
- 对象的属性没有赋值,该属性的值为undefined。
- 不指定函数返回值,默认返回undefined。
对于null
,一般用来表示
- 显式指定函数参数为
null
。 - 显式指定原型链的终点为
null
。
Object.getPrototypeOf(Object.prototype) //null
在设计上,二者的类型确不同,对于null
的类型为Object,这个已经被人吐槽好多次了,这里就不再解释了。
typeof undifined //undefined
typeof null //object
Exceptions
JavaScript使用throw
抛异常,当函数抛出异常后会立即终止运行
var add = function (a,b){
if(typeof a != 'number' || typeof b != 'number'){
throw{
name: 'TypeError',
message: 'add needs numbers'
};
}
return (a+b); //won't run if there is an error
}
var try_it = function(x,y){
try{
console.log(add(x,y));
}catch(e){
console.log(e.name, e.message)
}
}
try_it(1,"10");