由插件封装引出的一丢丢思考
今天看一个妹子写的canvas的插件,好羞愧啊,比我小还比我厉害得多,氮素,得向厉害的的人学习呀。所以就拜读了源码,业务方面的东西我就不说了,我也没仔细看,主要是被下面这一部分代码吸引了。
_global = (function() {
return this || (0, eval)('this');
}());
if (typeof module !== "undefined" && module.exports) {
module.exports = CanvasStar;
} else if (typeof define === "function" && define.amd) {
define(function() {
return CanvasStar;
});
} else {
!('CanvasStar' in _global) && (_global.CanvasStar = CanvasStar);
}
细细琢磨了一会,看懂了if
和else if
判断的用意。
在这之前先说明下CanvasStar
是什么。代码里有这样一句。
function CanvasStar() {}
所以这个方法就是在代码里执行这个canvas的入口,其他所有相关的内容都作为一个对象赋值给了他的原型对象。
再说回那两个判断,因为在es6之前,都用的是commonJS
和AMD
规范进行代码加载,所以含义就在于当前的环境支不支持commonjs
或者AMD
规范。在HTML
文件里引用的话,这两个就先跳过吧。主要看这两句。
//问题1
_global = (function() {
return this || (0, eval)('this');
}());
//问题2
else{
!('CanvasStar' in _global) && (_global.CanvasStar = CanvasStar);
}
我google了(0, eval)('this')
,有篇文章是这么说的:
无论如何方式调用
(0, eval)('this')
,返回的都是全局对象
所以问题1其实就是在将全局环境(也就是window)赋值给一个变量。我console
了this
,按理说,这里的this
指向的就应该是全局变量,为什么还要后面的代码重新指向全局呢?
然后打算重新看一遍代码的时候发现她在写这个插件的时候用的是严格模式,所以这里的this
只可能是underfined
。我贴一下MDN对于严格模式下this
的指向。
在严格模式下通过this传递给一个函数的值不会被强制转换为一个对象。对一个普通的函数来说,this总会是一个对象:不管调用时this它本来就是一个对象;还是用布尔值,字符串或者数字调用函数时函数里面被封装成对象的this;还是使用undefined或者null调用函数式this代表的全局对象(使用call, apply或者bind方法来指定一个确定的this)。这种自动转化为对象的过程不仅是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患,因为全局对象提供了访问那些所谓安全的JavaScript环境必须限制的功能的途径。所以对于一个开启严格模式的函数,指定的this不再被封装为对象,而且如果没有指定this的话它值是undefined
很长是吧,简短的说,在严格模式下,如果没有给this
指定值的话,它就是未定义的。所以在赋值的时候就跳过了这个this
,返回了(0, eval)('this')
。
这里说明一下eval
,在我找资料的过程中,都提到它的两种使用方式间接eval调用和直接eval调用,这两种的调用方式的结果完全不同,一般我见到的都是直接eval调用
,甚至于由于不提倡使用,所以eval
几乎很少出现。
等我在看多一点资料以后在写一个eval
相关的博文吧。但我可以先对这里面的逗号操作符做一点说明。
逗号操作符
这是MDN上的解释
逗号操作符 对它的每个操作数求值(从左到右),并返回最后一个操作数的值。
我就用几个代码说明一下
function func1() {
let a = '我是第一个赋值方法'
console.log('一号喵')
return a
}
function func2() {
let b = '我是第二个赋值方法'
console.log('二号喵')
return b
}
let c = (func1(), func2())
console.log(c)
猜猜这里有几个console
,分别是什么。
现在揭晓答案
//console.log结果
一号喵
二号喵
我是第二个赋值方法
所以根据定义来看,在对c
赋值的过程中,从左至右依次执行了func1
和func2
两个方法,但是在赋值的时候,只返回了最后的那个值,也就是func2
里写的return
。
所以我们在看一下eval
(0, eval)
这里返回的也是eval
,等同于这个
eval('this')
然而还是因为调用方式的不一样,所以最后的结果不一样,先按下不表了。
立即执行函数的公与私
那再来看问题2就简单明了多了,他就是在判断全局是否存在CanvasStar
这个方法,如果不存在,就在全局创建一个变量并将内部的方法赋值给他。
但这里就涉及一个问题,像是我,单独写js文件并引入使用的时候,都是直接调取方法使用,为什么这么麻烦啊,所以这里我也尝试在HTML文件里直接调用CanvasStar
(前提是把那些代码注释了)。
但很可惜,浏览器报错:
Uncaught TypeError: CanvasStar is not a constructor
所以这里我就想说说共有方法和私有方法,代码如下
//main.js
(function() {
let a = '猜猜我是什么类型'
function sum() {
console.log(a)
}
let log = function() {
console.log(a)
}
})()
然后html文件里调用:
sum(); // Uncaught ReferenceError: sum is not defined
log(); // Uncaught ReferenceError: log is not defined
我对main.js
的文件做一丢丢修改
//main.js
(function() {
let a = '猜猜我是什么类型'
log = function() {
console.log(a)
}
function sum() {
console.log(a)
}
})()
重新运行:
log(); // 猜猜我是什么类型
sum(); // Uncaught ReferenceError: sum is not defined
我第一次在js文件里写了一个函数声明和一个函数表达式,但是在外部都无法调用,第二次我把函数表达式赋值的变量声明去掉之后,就能正常访问了。
这个问题的关键在作用域,当我建立这个立即执行函数是,作用域链是这样的:
全局作用域 |
---|
匿名函数 |
函数作用域 |
---|
变量a |
log函数 |
sun函数 |
而当匿名函数执行完之后,它本身的作用域就被销毁了,从他的上一级,也就是全局作用域根本访问不到任何东西,但如果在进行函数赋值时,赋值的变量并没有经过var
或者let
生明,在这里log
这个变量是被写在全局作用域里面的,所以外部直接调用完全没问题。
所以得出的一个结论是:讲过let
或者var
生明的变量都是私有的,函数声明一定是私有的方法。其他都是共有变量或者方法。另外,共有方法能访问作用域里的私有变量,但是私有变量无法从外部直接获取。
其实这也就是某种意义上的闭包啦。
另一种封装方法
要是只讲上面的多没意思啊,正好我最近在看underscore的源码,我就想着看看人家的封装方法是啥。
在规范判断那一块大同小异,就不说了,但是对于全局变量的赋值走的是一条完全不同的路。
(function() {
let root = this;
............
.............
root._ = _
}.call(this))
这样外部直接
_.方法名;
就可以使用了。
那在这里,underscore在执行这段匿名函数的时候,使用call
将函数的this
指向了全局变量,这里就是this
,可能这句话比较绕,但事实就是这样。如果实在理解不了,我举个例子:
一艘船在海上航行,夜间,如果天空晴朗,指的是一般模式,那水手可以根据天上的星辰判断方位,如果不幸乌云密布,就是严格模式,那就迷路啦,但恰好,转过一个海湾,发现了一座著名的灯塔,重新给你指引了方向,这就是call重新指向当前作用域的this,也就是全局。
不知道我有没有说清楚呀。