你不知道的JS(一)

2021-04-23  本文已影响0人  SunnyCyx

1、RHS(Right-Hand-Side)查询与LHS(Left-Hand-Side)查询

“RHS 查询与简单地查找某个变量的值别无二致(a),而LHS 查询则是试图找到变量的容器本身,从而可以对其赋值(a=2)。“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”
RHS查询:相当于查找某个变量的值(a),如果是RHS查询,当引擎发现没有a变量的时候,会抛出ReferenceError异常。当查询到该变量时,如果该查询对变量进行了非正确的操作(非函数类型的值进行函数调用),就会抛出TypeError异常

LHS查询,相当于对某个变量进行赋值(a=2),如果是LHS查询,当引擎发现没有a变量时,会在改作用域创建一个a变量(在非严格模式下)


2、变量查找

“作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。”
全局变量会成为全局对象的属性,我们可以通过对全局对象的引用,来使用被遮蔽的全局变量。


3、欺骗词法

通过两种机制对词法作用域进行 “修改”。

1)、eval()

"eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码"

function foo(str, a){

    eval( str ); // 欺骗!--eval将var b=3写在了该位置,在该作用域中声明了一个新变量b

    console.log( a, b );

}

var b = 2;

foo( "var b = 3;", 1 ); // 1, 3

JavaScript 中还有其他一些功能效果和eval(..) 很相似。setTimeout(..) 和 setInterval(..) 的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的 函数代码。

2)、with关键字

with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对 象的属性也会被处理为定义在这个作用域中的词法标识符。

var str={a="kk",b="is",c="happpy"};

str.a="ss";//平常的引用,需要重复使用str来引用

with(str){a="ss";b="isn't";c="sad"}//快捷引用

function foo(obj) {

    with (obj) {a = 2;}

}

var o1 = {a: 3};

var o2 = {b: 3};

foo( o1 );

console.log( o1.a ); // 2

foo( o2 );

console.log( o2.a ); // undefined

console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!

因为在o2对象中,并不存在属性a,所以,不会创建a属性,o2.a=undefined,但是因为进行了LHS标识符查找,所以就创建了一个全局变量a。

所以如果使用with对对象进行属性读取的话,相当于进行了LHS标识符查询,当全局都不存在变量a的情况下,会在全局的作用域创建一个变量a

欺骗词法的方式对javaScript的性能会造成影响:

JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。

如果引擎在代码中发现了eval(..) 或with,它只能简单地假设关于标识符位置的判断 都是无效的,因为无法在词法分析阶段明确知道eval(..) 会接收到什么代码,这些代码会 如何对作用域进行修改,也无法知道传递给with 用来创建新词法作用域的对象的内容到底 是什么。

如果代码中大量使用eval(..) 或with,那么运行起来一定会变得非常慢。无论引擎多聪 明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代 码会运行得更慢这个事实。


4、作用域

javaScript具有基于函数的作用域

通过作用域,我们可以进行隐藏内部的实现

可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域 来“隐藏”它们。
可以规避冲突

可以避免同名标识符之间的冲突, 两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致 变量的值被意外覆盖。

function foo() {

    function bar(a) {

        i = 3; // 修改for 循环所属作用域中的i

        console.log( a + i );}

        for (var i=0; i<10; i++) { bar( i * 2 ); // 糟糕,无限循环了! }

}

foo();

函数与函数表达式

区分函数声明和表达式最简单的方法是看function 关键字出现在声明中的位 置(不仅仅是一行代码,而是整个声明中的位置)。如果function 是声明中 的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

var a = 2; 

(function foo(){ // <-- 添加这一行

    var a = 3;

    console.log( a ); // 3

})(); // <-- 以及这一行

console.log( a ); // 2

使用函数表达式,可以不需要显示的声明foo()后还有通过函数名调用这个函数才能使其运行。此时,foo 被绑定在函数表达式自身的函数中而不是所在作用域中。换句话说,(function foo(){ .. }) 作为函数表达式意味着foo 只能在.. 所代表的位置中被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。

匿名与具名

setTimeout( function() {

    console.log("I waited 1 second!");

}, 1000 );

这叫作匿名函数表达式,因为function().. 没有名称标识符。函数表达式可以是匿名的, 而函数声明则不可以省略函数名——在JavaScript 的语法中这是非法的。

IIFE,代表立即执行函数表达式 (Immediately Invoked Function Expression),第一个()将函数变成表达式,第二个()执行了这个函数。还有另一种形式:(function(){ .. }())。用于调用的()被移进了用来包装的()中
匿名函数表达式简单快捷,但由于匿名,不容易调试,在需要引用自身时,只能使用已经过期的arguments.callee引用,可读性差

块作用域

{

//块作用域

}

1、用with 从对象中创建出的作用域仅在with 声明中而非外 部作用域中有效。
functionfoo(obj) {

with(obj) {var a=2;console.log(a);//2}

}

varo2 ={b:3};

foo(o2);

console.log(o2.a);// undefined
2、ES3 规范中规定try/catch 的catch 分句会创建一个块作 用域,其中声明的变量仅在catch 内部有效。
3、ES6 中,引入了新的let 关键字,提供了除var 以外的另一种变量声明方式。 let 关键字可以将变量绑定到所在的任意作用域中(通常是{ .. } 内部)。换句话说,let 为其声明的变量隐式地了所在的块作用域。使用let 进行的声明不会在块作用域中进行提升(见5)。声明的代码被运行之前,声明并不 “存在”。

由于let 声明附属于一个新的作用域而不是当前的函数作用域(也不属于全局作用域), 当代码中存在对于函数作用域中var 声明的隐式依赖时,就会有很多隐藏的陷阱,如果用 let 来替代var 则需要在代码重构的过程中付出额外的精力。

4、ES6 还引入了const,同样可以用来创建块作用域变量,但其值是固定的 (常量)。之后任何试图修改值的操作都会引起错误

5、提升

提升是指声明会被视为存在于其所出现的作用域的整个范围内
引擎在解释js代码前,会先进行编译,编译中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来。所以var a=2中,var a是在编译阶段进行,a=2;会被留在执行阶段。

这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动” 到了最上面。这个过程就叫作提升。先有蛋(声明)后有鸡(赋值)。
函数声明会被提升,但是函数表达式却不会被提升

foo();

function foo() {

    console.log( a ); // undefined

    var a = 2;

}

foo(); // 不是ReferenceError, 而是TypeError!

var foo = function bar() { // ... };

函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个 “重复”声明的代码中)是函数会首先被提升,然后才是变量。

总结:引擎是先执行 词法作用域,所以声明会先被运行,其中函数声明会优先于变量声明。但是let的声明方式,是不能进行提升的。


6、作用域闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行。

function foo() {

    var a = 2;

    function bar() { console.log( a ); }

    return bar;

}

var baz = foo();

baz(); // 2 —— 朋友,这就是闭包的效果。

bar()在自己的词法作用域之外执行。在foo()执行之后,本该对其进行回收(引擎的垃圾回收器来释放不在使用的内存空间)。但由于存在闭包,bar()本身在使用内部作用域,所以该作用域可以一直存活。使得bar()可以在之后的任何时间进行引用。

循环与闭包

for (var i=1; i<=5; i++) {

    setTimeout( function timer() {

        console.log( i );

    },0 );

}//6 6 6 6 6 6

五个setTimeout都被封闭在一个共享的全局作用域中,所以只有一个i。

for (var i=1; i<=5; i++) {

    (function(j) {

        setTimeout( function timer()

        { console.log( j );

        }, j*1000 );

    })( i );

}//1、2、3、4、5

在迭代内使用IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的 作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

使用IIFE来创建一个函数作用域,这样,就会拥有五个相互独立的作用域

for (let i=1; i<=5; i++)

{

    setTimeout( function timer() {

        console.log( i ); }, 0 );

}//1 2 3 4 5

for 循环头部的let 声明还会有一 个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随 后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量
通过let来建立块级作用域,这样,也会拥有五个相互独立的作用域


7、模块

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
    return {
        doSomething: doSomething,
        doAnother: doAnother
    };
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

CoolModule()是一个函数,必须要通过调用它来创建一个模块实例。它的返回值是{key:value}的形式,通过这个形式来表示对象。在这个函数中,返回的是内部函数,而不是内部数据变量。所以变量是隐藏且私有的状态。doSomething()和doAnother()函数具有涵盖模块实例内部作用域的闭包。

模块模式需要具备两个必要条件:

1、必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
2、封闭函数必须至少返回一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。
var foo = (function CoolModule(id) {
function change() {
// 修改公共API
  publicAPI.identify = identify2;
}
function identify1() {
  console.log( id );
}
function identify2() {
  console.log( id.toUpperCase() );
}
var publicAPI = {
  change: change,
  identify: identify1
};
  return publicAPI;
})( "foo module" );
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

命名将要作为公共API返回的对象。通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块进行修改,包括添加删除方法、属性,以及修改他们的值。

8、this

this是在运行时被绑定的,并不是在编写时,他的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有关系,只取决于函数的调用方式。

/**使用call函数,使得this指向foo函数**/
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
// 注意, 在当前的调用方式下(参见下方代码), this 确实指向 foo
this.count++;
} 
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 call(..) 可以确保 this 指向函数对象 foo 本身
foo.call( foo, i );
}
} 
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 4
调用位置

调用位置是指函数在代码中被调用的位置。我们需要关注的是当前正在执行的函数的前一个调用中。

function baz() {
// 当前调用栈是: baz
// 因此, 当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此, 当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此, 当前调用位置在 bar 中
console.log( "foo" );
} b
az(); // <-- baz 的调用位置

调用的绑定规则

/**this指向全局对象**/
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
/****/
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2

首先需要注意的是 foo() 的声明方式, 及其之后是如何被当作引用属性添加到 obj 中的。但是无论是直接在 obj 中定义还是先定义再添加为引用属性, 这个函数严格来说都不属于
obj 对象。然而, 调用位置会使用 obj 上下文来引用函数, 因此你可以说函数被调用时 obj 对象“拥有” 或者“包含” 它。

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。 举例来说:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
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的一个引用,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"

参数传递是一种隐式赋值。所以obj.foo会被赋值给fn()。所以又是默认绑定。

function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
显式绑定仍然存在丢失绑定问题,可以采取硬绑定的方式解决
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2
function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
};
//辅助绑定函数
function bind(fn,obj){
    return function () {
        return fn.apply(obj,arguments)
    }
}
var bar = bind(foo,obj)
var a = "oops, global"; 
bar(); 

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

var bar = foo.bind(obj)
var a = "oops, global";
bar();

以上三种都是硬绑定的方式,还可以采用API调用的“上下文”

第三方库的许多函数, 以及 JavaScript 语言和宿主环境中许多新的内置函数, 都提供了一个可选的参数, 通常被称为“上下文”(context), 其作用和 bind(..) 一样, 确保你的回调函数使用指定的 this。这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定, 这样你可以少些一些代码。

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
};
[1].forEach(foo,obj)

构造函数只是一些使用 new 操作符时被调用的函数。 它们并不会属于某个类, 也不会实例化一个类。 实际上,它们甚至都不能说是一种特殊的函数类型, 它们只是被 new 操作符调用的普通函数而已。所以, 包括内置对象函数(比如 Number(..), 详情请查看第 3 章) 在内的所有函数都可以用 new 来调用, 这种函数调用被称为构造函数调用。

使用new进行函数调用的时候,会发生以下操作

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

优先级

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
console.log( bar.a ); // 4
上一篇下一篇

猜你喜欢

热点阅读