10段代码打通js学习的任督二脉

简单回调代码

function foo(){
    console.log(this.a);
}
function doFoo(fn){
    fn();
}
function doFoo2(o){
    o.foo();
}
var obj = {
    a: 2,
    foo: foo
};
var a = "I'm an a";
doFoo(obj.foo);
doFoo2(obj);

分析

在Javascript中,this指向函数 执行时的当前对象,而非声明环境有
执行doFoo的时候执行环境就是doFoo函数,执行环境为全局。
执行doFoo2时是在对象内部调用函数,this指针指向该对象。

结果

I'm an a
2

用apply改变函数作用域

function foo(somthing){
    console.log(this.a, somthing);
}
function bind(fn, obj){
    return function(){
        return fn.apply(obj, arguments);
    }
}
var obj = {
    a:2
}
var bar = bind(foo, obj);
var b = bar(3);
console.log(b);

分析

apply、call、bind都有个作用就是改变作用域,这里用apply将foo函数的作用域指向obj对象,同时传入参数。
再简单分析一下bind函数内部的嵌套,执行bind函数的时候返回的是一个匿名函数,所以执行bar(3)的时候实际上是执行的bind内部的匿名函数,返回的是之前传入的foo函数的执行结果。
函数没有返回值的情况下默认返回undefined。

结果

2 3
undefined

new关键字

function foo(a,b){
    this.val = a+b;
}
var bar = foo.bind(null, 'p1');
var baz = new bar('p2');
console.log(baz.val);

分析

bind函数的第一个参数为null代表作用域不变,后面的不定参数将会和函数本身的参数按次序进行绑定,绑定之后执行函数只能从未绑定的参数开始传值。

此处的this指向新创建的对象,之所以要在new中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数。bind(...)的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传给下层的函数(这种技术称为“部分应用”,是“柯里化”的一种)

结果

p1p2

自执行函数

function foo(){
    console.log(this.a);
}
var a = 2;
var o = {a:3,foo:foo};
var p = {a:4};
(p.foo=o.foo)();

分析

经常可以看到这样的代码

(function(){
    //...
})()

这种代码通常是创建一个立即执行的函数同时避免污染全局变量。
很少有人去关注赋值语句执行之后会返回什么结果,其实就是返回当前值。也就是说当括号内执行完赋值之后,返回的是o对象中的foo函数。函数的执行环境中有一个a对象,嗯,就是它了~

答案

2

变量属性

var a = [];
a[0] = 1;
a['foobar'] = 2;
console.log(a.length);
console.log(a.foobar);

分析

当一个变量被声明后,扩充其属性并不会改变原数据类型。

结果

1
2

精度问题

var a = 'foo';
a[1] = 'O';
console.log(0.1+0.2==0.3||a);

分析

当操作小数时请小心,js的小数计算并不精确,所以上面的判断是false。
字符串变量是常量。

结果

foo

命名提升

foo();
var foo = 0;
function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
};

分析

声明的变量和命名函数都会被提升到代码的最前面,只不过声明的变量的赋值语句在代码中的位置不变。所以上面这段代码应该被理解为:

var foo;
function foo(){
    console.log(1);
}
foo();
foo = 0;
foo = function(){
    console.log(2);
};

结果

1

思考

foo();
var foo = 0;
function foo(){
    console.log(1);
}
foo();
foo = function(){
    console.log(2);
};
foo();

上面代码的结果:

1
报错

作用域

foo();
var a = true;
if(a){
    function foo(){
        console.log('a');
    }
} else {
    function foo(){
        console.log('b');
    }
}

分析

javascript并不是以代码段为作用域,而是以函数。
再根据命名提升的原则,所以这段代码应该是这样的:

function foo(){
    console.log('a');
}
function foo(){
    console.log('b');
}
foo();
var a = true;
if(a){
} else {
}

结果

b

闭包陷阱

for(var i=1;i<=5;i++){
    setTimeout(function(){
        console.log(i);
    }, i*1000);
}

分析

闭包有个重要的作用就是,在内层函数引用外层函数定义的变量时,外层函数的变量不会被会被持久化。
这里有个隐藏陷阱就是for循环结束之后i仍然自增了1。

结果

6
6
6
6
6

伪闭包

function foo(){
    console.log(a);
}
function bar () {
    var a = 3;
    foo();
}
var a = 2;
bar();

分析

闭包是函数的嵌套定义,而不是函数的嵌套调用。

结果

2

思考

如何输出3?

function bar () {
    function foo(){
        console.log(a);
    }
    var a = 3;
    foo();
}
var a = 2;
bar();

彩蛋

光说不练假把式~
一周月内将下题正确答案发送至我邮箱内(邮箱地址请参考博客),将获得本年度我阅读过最优秀的关于AngularJS的电子书一本。

var Obj = {
    name: 'zdl',
    do: function(){
        console.log(this.name);
    }
}

写个对象a继承Obj的方法(不使用new)。

我们所需要做的就是找到函数的调用位置并判断应用哪条规则,不过,如果某个调用位置可以应用多条规则该怎么办?为了解决这个问题就必须给这些规则设定优先级,对于正常的函数调用来说,优先级为 new绑定 > 显示绑定 > 隐式绑定 > 默认绑定,所以我们可以按照下面的顺序来进行判断:

  1. 函数是否在new中调用(new 绑定) ? 如果是的话this绑定的是新创建的对象。
    var bar = new foo() //此处foo函数里面的this指向bar
    注: 可以在new中使用硬绑定函数,如下
function foo(p1,p2){
    this.val = p1 + p2;
}
var bar = foo.bind(null,'p1')
var baz = new bar('p2')
console.log(baz.val);//p1p2

此处的this指向新创建的对象,之所以要在new中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数。bind(...)的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传给下层的函数(这种技术称为“部分应用”,是“柯里化”的一种)

  1. 函数是否通过call、apply(显示绑定)或者bind(硬绑定)调用?如果是的话,this绑定的是指定的对象
    var bar = foo.call(obj2) //此处foo函数里的this指向obj2
  2. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象(注意有隐式丢失的情况)
    var bar=obj1.foo() //此处this指向obj1
  3. 如果都不是的话,使用默认绑定,严格模式下this绑定到undefined,非严格模式下绑定到全局对象
    var bar = foo() //此处foo函数里面的this指向全局对象

但是在某些场景下this的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则,这被称为绑定例外。

被忽略的this

  • 当把null或者undefined作为this的绑定对象传入call、apply或者bind时,,这些值在调用的时候会被忽略,实际应用的是默认绑定规则:
function foo(){
    console.log(this.a);
}
var a = 2;
foo.call(null); //2   所以此处foo函数里的this指向全局对象

一般在什么样的情况下我们会传入null呢,比如某个函数接收多个参数,但此时我们想传入的却是一个数组,便可以使用apply(...)来接收这个参数数组,类似地,使用bind(...)可以对参数进行柯里化(预先设置一些参数)

function foo(a,b){
    console.log("a:"+a+",b:"+b);
}
//通过使用apply传入数组
foo.apply(null,[2,3]);//a:2,b:3

//使用bind进行柯里化
var bar = foo.bind(null,1);
bar(2);//a:1,b:2

这两种方法都需要传入一个参数当作this的绑定对象。如果函数内部根本不关心this的话,我们仍需要传入一个占位符,这时null便成了一个不错的选择,在ES6中,可以用...扩展运算符代替上面的apply方法展开数组,这样可以避免不必要的this绑定,但仍然没有柯里化的相关语法,因此还是需要使用bind(...)
  然而,总是使用null来忽略this绑定可能产生一些副作用,如果某个函数内部确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(在浏览器中这个对象是window),这将导致无法预料的后果(比如修改了全局对象的属性),显而易见,这种方式可能会导致许多难以分析和追踪的bug.
  一种“更安全”的做法是传入一个特殊的对象,把this绑定到这个对象不会对我们的程序产生任何副作用。就像网络(以及军队)一样,我们可以创建一个“DMZ“(demilitarized zone,非军事区)对象--它就是一个空的非委托对象(何为非委托对象?后续继续了解).如果我们在忽略this绑定时总是传入一个DMZ对象,那就什么都不用担心了,因为任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。一般可使用∅变量名来表示这个空对象。在JavaScript中创建一个对象最简单的方法是Object.create(null)。它和{}很像,但是并不会创建Object.prototype这个委托,所以它比{}“更空”:

function foo(a,b){
    console.log("a:"+a+",b:"+b);
}
const ∅=Object.create(null);
foo.apply(∅,[2,3]);  //a:2,b:3
const bar=foo.bind(∅,2);
bar(3);  //a:2,b:3

通过使用∅这个空对象不仅让函数变得更加“安全”,而且可以提高代码的可读性。

  • 另一种this被忽略的情况发生在隐式绑定被间接引用时,此时,会应用默认绑定规则。
    常见的间接引用有两种情况:
    赋值:将某个隐式绑定的函数句柄赋值给一个变量,然后通过这个变量执行函数,这便属于间接引用,函数里的this将应用默认绑定规则,不再指向隐式绑定的那个对象
    当作参数传入函数时,一般作用回调函数使用,其实传参相当于一种隐式赋值,和上面一样,应用默认绑定规则。
    注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。

软绑定

之前我们已经看到过,硬绑定这种方式可以把this强制绑定到指定的对象(除了使用new时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this。
如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力。
可以通过一种被称为软绑定的方法来实现我们想要的效果:

if(!Function.prototype.softBind){
    Function.prototype.softBind=function(obj){
        const fn = this;//这里的this取决于softBind函数的调用位置
        const carried = Array.prototype.slice.call(arguments,1);
        const bind = function(){
            return fn.apply(
                (!this||this===window)?obj:this,//这里的this由软绑定后的函数调用位置决定,注意,与上面的this不同
                Array.prototype.concat.apply(carried,arguments)
           );
        }
        bind.prototype = Object.create(fn.prototype);
        return bind;
    }
}

除了软绑定之外,softBind(...)的其他原理和ES5内置的bind(...)类似,它会对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不修改this。此外,这段代码还支持可选的柯里化。

function foo(){
    console.log("name:"+this.name);
}
const obj1={name:"obj1"},obj2={name:"obj2"},obj3={name:"obj3"};
const fooObj=foo.softBind(obj1);
fooObj();//name:"obj1"  软绑定
obj2.foo=foo.softBind(obj1);
obj2.foo();//name:"obj2"  隐式绑定修改了this
fooObj.call(obj3);//name:"obj3"  显示绑定修改了this

setTimeout(obj2.foo,100);//name:"obj1"  当应用默认绑定时,则会应用我们自定义的软绑定(在node环境下显示undefined,不知此处为何依然应用了默认绑定)

注意:箭头函数

在ES6中定义了一种新的函数类型:箭头函数,箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的,箭头函数本身取消了this机制,使用更常见的词法作用域替代this,箭头函数里的this继承自外层函数的this。箭头函数的绑定无法被修改,new也不行。

以上内容来自简书与慕课。

猜你喜欢

转载自blog.csdn.net/qq_21423689/article/details/81505092