你不知道的js——上

183 阅读8分钟

第一部分 作用域与闭包

(一)词法作用域

欺骗作用域:eval 和 with 不建议使用

var obj = {
    a:1,
    b:2,
    c:3
    
}
//重复obj修改
obj.a = 3;
obj.b = 4;
obj.c = 5;
//快速修改
with(obj){
    a=3,
    b=4,
    c=5
}

(二)函数作用域和块作用域

函数表达式:

代码中foo 被绑定在函数表达式自身的函数中而不是所在作用域中。换句话说,(function foo(){ .. }) 作为函数表达式意味着foo 只能在.. 所代表的位置中被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。

块作用域:

1.with

2.try/catch

catch分句会创建一个块作用域,其中生命的变量仅在catch内部有效。

3.let

使用let进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不存在。

  • 垃圾回收 在块中声明的变量,在使用后可以被回收。

4.const

作用域闭包

闭包:将内部函数传递到所在的词法作用域外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。


现代模块机制

var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
         modules[name] = impl.apply( impl, deps );
    }
    function get(name) {
        return modules[name];
    }
   
    return {
        define: define,
        get: get
    };
})();
MyModules.define( "bar", [], function() {
    function hello(who) {
        return "Let me introduce: " + who;
    }
    return {
        hello: hello
    };
} );
MyModules.define( "foo", ["bar"], function(bar) {
    var hungry = "hippo";
    function awesome() {
        console.log( bar.hello( hungry ).toUpperCase() );
    }
    return {
        awesome: awesome
    };
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
    bar.hello( "hippo" )
); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

二 this和对象原型

  • 每个函数的this是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。

绑定规则

默认绑定

function foo() {
    console.log( this.a );
}
var a = 2;
foo(); // 2

在本例中,函数调用时应用了this 的默认绑定,因此this 指向全局对象。。在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。严格模式下与foo()不影响默认绑定。

隐式绑定

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

当foo() 被调用时,它的落脚点确实指向obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this 绑定到这个上下文对象.

function foo() {
    console.log( this.a );
}
var obj = {
   a: 2,
   foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

虽然bar 是obj.foo 的一个引用,但是实际上,它引用的是foo 函数本身,因此此时的bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    // fn 其实引用的是foo
    fn(); // <-- 调用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。

显式绑定

call、apply 1 硬绑定

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
     };
}
  var obj = {
     a:2
  };
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3

bind(..) 会返回一个硬编码的新函数,它会把参数设置为this 的上下文并调用原始函数。

new绑定

在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new 初始化类时会调用类中的构造函数。 在JavaScript 中,构造函数只是一些使用new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。 实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new 操作符调用的普通函数而已。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[ 原型]] 连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new 表达式中的函数调用会自动返回这个新对象。
function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

使用new 来调用foo(..) 时,我们会构造一个新对象并把它绑定到foo(..) 调用中的this上。new 是最后一种可以影响函数调用时this 绑定行为的方法,我们称之为new 绑定。

优先级

function foo() {
    console.log( this.a );
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

显式绑定优先级更高

function foo(something) {
    this.a = something;
}
var obj1 = {
    foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2

obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2//new不改变被new函数的值,只是改变新创建函数的
console.log( bar.a ); // 4//************

new绑定优先级高于隐式绑定


function foo(something) { 
    this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3); 
console.log( obj1.a ); // 2 
console.log( baz.a ); // 3

硬绑定优先级高于new ????是否和顺序有关

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

function foo(p1,p2) { this.val = p1 + p2;
}
// 之所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么 // 反正使用 new 时 this 会被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" ); baz.val; // p1p2

判断this顺序: 1 是否用new 2 是否硬绑定 3 是否隐式绑定 4 以上都不是

忽略this的情况

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 ); bar( 3 ); // a:2, b:3

总是使用 null 来忽略 this 绑定可能产生一些副作用。如果某个函数确实使用了 this(比如第三方库中的一个函数),那默认绑定规则会把 this 绑定到全局对象(在浏览 器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)。


function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象
var ø = Object.create( null ); 

// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3

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

Object.create(null)和{}类似,但是不会创建object.proptotype这个委托,所以比{}更空。?????

软绑定

硬绑定会大大降低函数的灵活性,使 用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。 ????????下方例子中没有发现这个情况。。。

function foo(something) { 
    console.log( this.a, something ); 
    return this.a + something;
}
function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
    }; }
var obj = { a:2};
var obj1 = {a:5}
var bar = bind( foo, obj );
bar(2)//4
bar = bind(foo,obj1);
bar(3)//8

软绑定

if (!Function.prototype.softBind) {                       Function.prototype.softBind = function(obj) {
    var fn = this;
    // 捕获所有 curried 参数
    var curried = [].slice.call( arguments, 1 ); 
    var bound = function() {
        return fn.apply(
            (!this || this === (window || global)) ?
                obj : this
            curried.concat.apply( curried, arguments )
        ); };
        bound.prototype = Object.create( fn.prototype);
        return bound; 
    };
}

它会对指定的函 数进行封装,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把 指定的默认对象 obj 绑定到 this,否则不会修改 this。


function foo() {
    console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" }, 
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );

fooOBJ(); // name: obj

obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!

fooOBJ.call( obj3 ); // name: obj3 <---- 看!  

setTimeout( obj2.foo, 10 );// name: obj <---- 应用了软绑定

Nodejs环境里面setTimeout的this非window或global,会指向TimeOut函数,所以最后会输出undefined。

箭头函数

箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。

function foo() {
    // 返回一个箭头函数 return (a) => {
    //this 继承自 foo()
    console.log( this.a ); 
    };
 }
 var obj1 = {
    a:2
};
var obj2 = { 
    a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !

foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不 行!)

class的this问题

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错。

一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}

另一种解决方法是使用箭头函数。

class Obj {
  constructor() {
    this.getThis = () => this;
  }
}

const myObj = new Obj();
myObj.getThis() === myObj // true

箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。