学了很久js,开始的时候自己最容易搞蒙的就是函数的abc三个方法:apply、bind、call。对于一些进阶应用,比如组合式继承,柯里化,不掌握这三个方法就很难理解复杂的用法。而js的执行环境,包括v8引擎的优化,之前写的笔记也是单独用了一部分篇幅。索性就将这两部分整理到一起写了。(建议先了解闭包)
犀牛书核心参考部分的例子(p766):
apply:
-
Object.prototype.toString.apply(o); -
var data=[2,24,6,123]; Math.max.apply(null,data);
bind:
es5新增方法。 假设f是一个函数,调用bind()方法如下:
var g=f.bind(o,1,2);
这样g就成为了一个新函数。调用g(3)等价于:
f.call(o,1,2,3);//后面会提到,bind这种调用方式其实就是柯里化。
call:
Object.prototype.toString.call(o);
apply和call是调用一个函数,bind是返回一个函数。
以前看犀牛书 没发现这么多错误,总共三个示例印错两处。
看完了这部分直接跳到柯里化,以前费很大劲才能理解的内容,这次一下子懂了。主要是因为过去基础不扎实,对arguments的理解就不到位,arguments是属于拥有它的函数的,之前讲的执行环境创建变量对象里有三件事:arguments创建,函数声明,变量声明。还有对于词法作用域的理解。后来闭包看多了就习惯了。。。
柯里化
柯里化函数的写法:
function curry(fn){
var args=Array.prototype.slice.call(arguments,1);
return function(){
var innerarg= Array.prototype.slice.call(arguments);
var finalarg=args.concat(innerarg);
return fn.apply(null,finalarg);
}
}
通过闭包,把传入的第一部分参数固定住。以后再调用,传入剩余的参数就可以了。通过这种形式,可以把参数分成两部分,一部分固定参数,一部分是柯里化后表面上需要引用的参数。
function add(num1,num2){
return num1+num2;
}
var curriedAdd=curry(add,3);
console.log(curriedAdd(5));//8
或者
function add(num1,num2){
return num1+num2;
}
var curriedAdd=curry(add,3,5);
console.log(curriedAdd());//8
再复杂点的是下面的bind()函数
function bind(fn,context){
var args=Array.prototype.slice.call(arguments,2);
return function(){
var innerarg=Array.prototype.slice.call(arguments);
var finalarg=args.concat(innerarg);
return fn.apply(context,finalarg);
}
}
函数和绑定对象,还有参数提前传入。
使用方式如下:
var needbindfunc=bind(fn,contextobj,arg1,arg2);
needbindfunc(arg);
这算是函数绑定和柯里化的一个结合。 对于红皮书(js高程)p606的示例,EventUtil源代码在p354页。
偶然发现js高程发行第四版了。。。中文版估计会抢手一波,必入!相比犀牛、js高程看起来更舒服。
var handler = {
message: "Event handled",
handleClick: function(name, event){
alert(this.message + ":" + name + ":" + event.type);
}
};
var btn = document.getElementById("my-btn");
addHandler(btn, "click", bind(handler.handleClick, handler, "my-btn"));
addHandler: function (element, type, handler) {
if (element.addEventListener) { //DOM2级
element.addEventListener(type, handler, false);//event会传入handler内
} else if (element.attachEvent) { //DOM1级
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler; //DOM0级
}
}
addHandler分别接受三个参数,dom元素、事件类型、处理函数
bind返回柯里化后的只接受event参数的函数。”my-btn”通过闭包和其他参数一起进入到函数。
es5的bind方法也实现了柯里化。
尝试模拟es5的bind实现:
Function.prototype.bind=function(context){
var args=Array.prototype.slice.call(arguments,1);
var that=this;
return function(){
var innerarg=Array.prototype.slice.call(arguments);
var finalarg=args.concat(innerarg);
return that.apply(context,finalarg);
}
}
var g=f.bind(o,1,2);
其实这么写存在一些问题,实际应用中,可能需要进行一些基本判断,比如调用bind的函数对象是否合法,原型链继承,还有关于this指向的问题。 假如我们这么调用,通常我们希望g是f绑定传递的对象和参数后的实现。如果把f看成构造函数,f就可能有自己的原型方法,那么理应继承到f的原型。通过上面我写的bind函数是无法继承的。因此,需要额外实现继承f的原型。
//转载自网络
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)));
};
if (this.prototype) {
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
}
return fToBind.apply(this instanceof fNOP ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)));
首个参数为什么出现this instanceof fNOP ?this : oThis || window?这涉及到构造函数和this机制的问题。
这个英文解释说的非常清晰:stackoverflow.com/questions/5…
上一篇笔记举过一个例子
var o = {
m: function(){
return this;
}
}
var obj=new o.m();
console.log(obj,obj === o);//{} false
console.log(obj.constructor === o.m);
当函数作为构造函数调用时,则this是构造函数返回的对象。
上面bind例子的简化版本
Function.prototype.bind = function( obj ) {
var self = this,
nop = function () {},
bound = function () {
return self.apply( this instanceof nop ? this : obj, arguments );
};
nop.prototype = self.prototype;
bound.prototype = new nop();
return bound;
};
对于上面的bind调用测试:
var obj = {};
function foo(x) {
this.answer = x;
}
var bar = foo.bind(obj); // "always" use obj for "this"
bar(42);
console.log(obj.answer); // 42
var other = new bar(1); // Call bar as a constructor
console.log(obj.answer); // Still 42
console.log(other.answer); // 1
当使用new bar()时,实际上就相当于是bind方法里的new bound(),这种情况this instanceof nop等于true,bar绑定的就是构造对象。正常调用bar()时,则绑定之前的上下文对象。
函数的简单赋值,往往会丢失原来的对象,this没办法指向正确的内容。如不进行处理,会导致运行出错。bind就能很容易的解决这个问题。
自己实现的还有一个问题:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
console.log(this.x + ',' + this.y);
};
var p = new Point(1, 2);
p.toString(); // '1,2'
var emptyObj = {};
var YAxisPoint = Point.bind(null, 0/*x*/);
var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5' axisPoint instanceof Point; // true axisPoint instanceof YAxisPoint; // true
console.log(new Point(17, 42) instanceof YAxisPoint); //true
最后一行代码输出为true,当然,这是想当然需要的结果,但是看不到v8里bind的源代码,所以就有点困惑。而且,换成上面自我实现的bind写法,这个地方会是false。
一个无限柯里化的例子:
//转载于网络
function add() {
var args =[].slice.call(arguments);
return function () {
var inargs = [].slice.call(arguments);
if( arguments.length == 0 ){
var me = 0 ;
for(var i in args){
me += args[i];
}
return me ;
}
else
return add.apply(null, args.concat(inargs));
//上面这一句说明最后是返回自身,因此可以持续柯里化
};
}
add(1)(2)();//3
bind()也可以为需要特定this值的函数创造捷径。
例如要将一个类数组对象转换为真正的数组,可能的例子如下:
var slice = Array.prototype.slice;
// ...
slice.call(arguments);
如果使用bind()的话,情况变得更简单:
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(unboundSlice);
// ...
slice(arguments);
反柯里化
反柯里化就是把
o.method(p1,p2);这种调用方式转变为method(o,p1,p2);。这样带来的好处,就是扩大了对象方法的适用范围,可以方便的进行方法借用。而函数是可以作为值传递的,所以又由此会引申出很多其他的调用方式。怎么解决从o.method(p1,p2)到method(o,p1,p2)呢?
Function.prototype.uncurrying = function() {
var that = this;
return function() {
return Function.prototype.call.apply(that, arguments);
}
};
其实反柯里化的写法也有好多种,也可以这么写:
return that.apply(arguments[0],[].shift.call(arguments));
看起来这种方式更扁平不那么绕。
调用如下:
var o={
value:"tiedan",
sayHi:function(){
return "Hello " + this.value +" "+[].slice.call(arguments);
}
};
var sayHiuncurrying=o.sayHi.uncurrying();
console.log(sayHiuncurrying({value:'world'},"hahaha"));
和调用call相比:
o.sayHi.call({value:'world'},"hahaha")
在调用方式上方便了。尤其需要大量这种调用的情况。?仔细思考一下反柯里化的转变形式。
其实把uncurring封装成函数更好:
var uncurrying= function (fn) {
return function () {
return fn.apply(arguments[0],[].shift.call(arguments));
}
};
执行环境
对于执行上下文来说,它的生命周期由三部分组成:
- 创建:变量对象,作用域链,this指向
- 执行:变量对象变为活动对象,可以访问。即可赋值,函数引用,或其他代码操作。
- 执行完毕:被推出栈,等待GC
对于函数的创建变量对象部分,有三部分内容, arguments,函数声明,变量声明。
- arguments参数对象创建,是个类数组结构
- 函数声明可用新的引用覆盖掉之前同名的声明。
- 变量声明如果重复则会跳过。如果没有初始化的变量,统一给undefined。
由二三点可得出一个结论,函数的优先级比变量高,并且变量是无法覆盖掉同名函数声明的。
v8引擎
v8引擎是用c++编写的开源项目,同时是可以和chrome剥离的。业界最快的js引擎。据说比firefox和safari快10倍,比ie快50倍。
JIT技术是v8的一种加速方式,代码不转换中间字节码,直接转机器码。V8是将源代码进行词法分析和语法分析,得到抽象语法树,然后直接将抽象语法树编译成机器代码。
快速属性访问,用到隐藏类和内嵌缓存技术。可以减少访问hash表,提高访问速度。
动态机器码生成,转换为机器码后,还需要动态的对代码进行优化处理。处理回滚等。
高效的垃圾收集,与java的hotspot类似的机制,增量标记+延迟清理
隐藏类要强调属性初始化赋值的顺序,顺序错了,就会剔除优化,影响效率。所以在习惯上要适应这一点!
虽然经常说js是一门解释型语言,但是js发展到今天,为了处理越来越复杂的应用。例如一些office应用都搬到浏览器环境了。为了提高运行效率。处理js的引擎也有了很大的进步。对于v8和其他一些js引擎来说,处理js代码都会进行编译。