你所不知道的 JavaScript
目录
- 一、作用域和闭包
- 附录B 块作用域的替代方案
- 1.1 Traceur - 将ES6 代码生成兼容ES5的工具
- 1.2 隐式和显式作用域
- 附录C this 词法
- 二、this 和对象原型
- 关于 this
- 1.1 关于的错误认识
- 1.2 this 是什么
- this 的全面解析
- 2.1 调用位置
- 对象
- 3.1 语法
- 3.2 类型
- 3.3 对象内容 - 属性
- ~ 未完待续~
一、作用域和闭包
1. 附录B 块作用域的替代方案
1.1 Traceur - 将ES6 代码生成兼容ES5的工具
Google 维护着一个名为 Traceur 的项目,该项目正是用来将ES6 代码转换成兼容 ES6 之前的环境(大部分是ES5,但不是全部)。TC39 委员会依赖这个工具(也有其他工具)来测试他们指定的语义化相关的功能。
Traceur 会将我们的代码片段转换成的样子:
{
try {
throw undefined;
} catch (a) {
a = 2;
console.log( a );
}
}
console.log( a );
1.2 隐式和显式作用域
let 作用域:
let (a = 2) {
console.log( a ); // 2
}
console.log( a ); // ReferenceError
let
声明会创建一个显示的作用域并与其进行绑定。
但是这里有一个小问题,let 声明并不包含在 ES6 中。官方的 Traceur 编译器也不接受这种形式的代码。
可以通过 let-er 工具将其转换成合法的、可以工作的代码。
let-er:一个构建时的代码转换器,但它 唯一 的作用就是找到 let 声明并对其进行转换。它不会处理包括 let 定义在内的任何其他代码。可以安全地将 let-er 应用在 ES6 代码转换的第一步,如果有必要,接下来也可以把代码传递给 Traceur 等工具。
此外,let-er 还有一个设置项 – es6,开启它(默认是关闭的)会改变生成代码的种类。开启这个设置项时 let-er 会生成完全标准的 ES6 代码,而不会生成通过 try/catch
进行 hack 的 ES3 替代方案:
{
let a = 2;
console.log( a );
}
console.log( a ); // ReferenceError
因此,在ES6 之前的所有环境中使用 let-er,开启设置项就会生成标准的 ES6 代码。
2. 附录C this 词法
ES6 中用箭头函数的方式将this
同词法作用域联系起来:
var foo = a => {
console.log( a );
};
foo( 2 ); // 2
普通函数容易丢失同this
之间的绑定。解决该问题有好几种办法,但最常用
的是 var self = this;
。
这里,self 是一个可以通过词法作用域和闭包进行引用的标识符,它不关心this
绑定的过程中发生了什么。
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout( () => { // 箭头函数是什么鬼东西?
this.count++;
console.log( "awesome?" );
}, 100 );
}
}
};
obj.cool(); // 很酷吧?
箭头函数 在涉及this
绑定时的行为和 普通函数 的行为完全不一致,它放弃了所有普通this
绑定的规则,取而代之的是用当前的词法作用域覆盖了this
本来的值。
因此,上面这个箭头函数只是“继承”了cool()
函数的this
绑定(调用它并不会出错)。
箭头函数缺点:它们是匿名而非具名的(具名函数比匿名函数更可取)。
因此,一个更合适的办法是正确使用和包含this
机制:
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout( function timer(){
this.count++; // this 是安全的
// 因为bind(..)
console.log( "more awesome" );
}.bind( this ), 100 ); // look, bind()!
}
}
};
obj.cool(); // 更酷了。
无论你是喜欢箭头函数中 this 词法的新行为模式,还是喜欢更靠得住的 bind(),都需要注意箭头函数不仅仅意味着可以少写代码。
二、this 和对象原型
this
提供了一种优雅的方式来隐式“传递”一个对象引用,而显式传递上下文对象会让代码变得越来越混乱。
1. 关于 this
1.1 关于的错误认识
太拘泥于“this”的字面意思就会产生一些误解。主要存在两种常见的对于this 的错误解释。
误解① - 指向自身
我们很容易把
this
理解成指向函数自身,这个推断从英语的语法角度来说是说得通的。那么为什么需要从函数内部引用函数自身呢?常见的原因是 递归(从函数内部调用这个函数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器。
下面是记录函数
foo
被调用的次数的示例,可看到this
并不像想像中的那样指向函数本身:function foo(num) {
console.log( "foo: " + num );
// 记录foo 被调用的次数
this.count++;
}
foo.count = 0; // 向对象foo添加属性count,并赋值0
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// 打印 foo 被调用次数
console.log( foo.count ); // 0 -- WTF?
console.log
语句产生了4 条输出,证明foo(..)
确实被调用了4 次,但是foo.count
仍然是0。显然从字面意思来理解this
是错误的。执行
foo.count = 0
时,向函数对象 foo 添加了一个属性 count,创建的是一个全局变量count
。函数内部代码this.count
中的this
并不是指向那个函数对象。
如果要从函数对象内部引用它自身,只使用 this 是不够的。一般来说需要通过一个指向函数对象的词法标识符(变量)来引用它。
function foo() {
foo.count = 4; // foo 指向它自身
}
setTimeout( function(){
// 匿名(没有名字的)函数无法指向自身
}, 10 );
第一个函数被称为具名函数,在它内部可以使用 foo 来引用自身。第二个传入`setTimeout(..)` 的 回调函数 没有名称标识符(匿名函数),因此无法从函数内部引用自身。
因此,对于本例,另一种解决方法是使用 foo 标识符替代 `this` 来引用函数对象:
function foo(num) {
console.log( "foo: " + num );
// 记录foo 被调用的次数
foo.count++;
}
foo.count=0
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
然而,这种方法同样回避了`this`的问题,并且完全 依赖于 变量 foo 的词法作用域。
另一种方法是强制 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 );
}
}
console.log( foo.count ); // 4
误解② - this的作用域
第二种常见的误解是,this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它是正确的,但是在其他情况下它却是错误的。
注意:
this
在 任何情况下都不指向函数的词法作用域 。在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于JavaScript 引擎内部。
下面来看一个使用`this`来 隐式 引用函数的 词法作用域 的示例:
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined
> 这段代码出自一个公共社区中互助论坛的精华代码。这段代码非常完美(同时也令人伤感)地展示了this 多么容易误导人。
这里,它试图跨界,但没有成功!
* 首先,这段代码试图通过 `this.bar()` 来引用 `bar()` 函数。这是绝对不可能成功的。调用`bar()`最自然的方法是省略前面的`this`,直接使用词法引用标识符。
* 此外,编写这段代码的开发者还试图使用`this` 联通`foo()` 和`bar()` 的词法作用域,从而让`bar()`可以访问`foo()`作用域里的变量`a`。这是不可能实现的,你不能使用`this`来引用一个词法作用域内部的东西。
因此,当你想要把`this`和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。
1.2 this 是什么
this
是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式 。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数 在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this
就是记录的其中一个属性,会在函数执行的过程中用到。
【小结】
- this 既 不指向 函数自身,也 不指向 函数的词法作用域;
- this 实际上是在 函数被调用时发生的绑定,它 指向 什么完全 取决于 函数在哪里被调用。
2. this 的全面解析
通过前面的学习,我们明白了每个函数的this
是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。
2.1 调用位置
理解调用位置:就是函数在代码中被调用的位置(而不是声明的位置)。
2.1.1 分析调用栈
只有仔细分析调用位置才能回答 “ 这个
this
到底引用的是什么?”寻找调用位置,最重要的是要分析调用栈。来看看到底什么是调用栈和调用位置:
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" );
}
baz(); // <-- baz 的调用位置
要能分析出真正的调用位置的,因为它决定了
this
的绑定。可以把调用栈想象成一个函数调用链,就像我们在前面代码段的注释中所写的一样。但是这种方法非常麻烦并且容易出错。另一个查看调用栈的方法是使用浏览器的调试工具。绝大多数现代桌面浏览器都内置了开发者工具,其中包含JavaScript 调试器。就本例来说,你可以在工具中给foo() 函数的第一行代码设置一个断点,或者直接在第一行代码之前插入一条debugger;语句。运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表,这就是你的调用栈。因此,如果你想要分析this 的绑定,使用开发者工具得到调用栈,然后找到栈中第二个元素,这就是真正的调用位置。
2.1.2 绑定规则
先调用位置,再判断
this
的绑定对象。而判断需要应用下面四条规则中的一条:- 默认绑定
- 隐式绑定
- 显式绑定
- new绑定
**1)默认绑定**
独立函数调用(无法应用其他规则时的默认规则,最常用)。对于直接使用不带任何修饰的函数引用进行调用的,只能使用 默认绑定,无法应用其他规则。
如果使用严格模式(`strict mode`),全局对象将无法使用*默认绑定*,因此`this`会绑定到 undefined。
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
这里,虽然`this`的绑定规则完全取决于调用位置,但是只有`foo()`运行在非`strict mode`下时,默认绑定才能绑定到全局对象;**严格模式下与**`foo()`**调用位置无关** 。
**2)隐式绑定**
**隐式绑定** 时,必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把`this`间接(隐式)绑定到该对象上。
**参数传递** 就是一种隐式赋值,因此传入函数的也会被隐式赋值;
当函数引用有上下文对象时, 隐式绑定 规则会把函数调用中的`this`绑定到这个上下文对象。
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
对象属性引用链中,只有最顶层或者最后一层会影响调用位置。
**隐式丢失**: 有个最常见的`this`绑定问题,就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把`this`绑定到全局对象或者`undefined`上(取决于是否是 严格模式)
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()`是一个不带任何修饰的函数调用,因此应用了 默认绑定 。
**3)显示绑定**
如果不想在对象内部包含函数引用,而想 在某个对象上 **强制调用函数**,可以使用函数的`call(..)`和 `apply(..)`方法。
--------------------
`call(..)`和`apply(..)`的**工作原理**:
它们的第`1`个参数是一个**对象**,这个对象会绑定到`this`,在调用函数时指定这个`this`。
--------------------
像这种直接指定`this`绑定对象的绑定,就称之为 **显式绑定** 。
如下所示:
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
> 1、从this绑定的角度来说,call(…) 和 apply(…) 是一样的,它们的区别体现在其他的参数上。
* **① 硬绑定**
显式绑定仍然无法解决丢失绑定问题;但是,硬绑定(显示绑定的一个变种)可以做到。
【工作原理】
`Function.prototype.bind(..)`会创建一个新的包装函数,该函数会忽略它当前的`this`绑定(无论绑定的对象是什么),并把我们提供的对象绑定到`this`上。
如下所示:
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
这里创建了函数`bar()`,并在它的内部手动调用了`foo.call(obj)`,因此强制把`foo`的`this`绑定到了`obj`。无论之后如何调用函数`bar`,它总会手动在`obj`上调用`foo`。这种显式的强制绑定,即称之为 **硬绑定** 。
【硬绑定应用场景】
创建一个 包裹函数,传入所有的参数并返回接收到的所有值:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5
由于硬绑定是一种非常常用的模式,所以在**ES5**中提供了内置的方法`Function.prototype.bind`,其用法如下:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
`bind(..)`的功能之一就是可以把除了第`1`个参数(第1个参数用于绑定`this`)之外的其他参数都传给下层的函数(这种技术称为“部分应用”,是“柯里化”的一种)。
> `bind(..)`会返回一个硬编码的新函数,它会把参数设置为`this`的上下文并调用原始函数。
* **② API 调用的上下文**
第三方库的许多函数,以及JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个 可选的 参数,通常被称为 “**上下文** ”(context)。
**作用:** 和`bind(..)` 一样,—— 确保回调函数使用指定的`this`。
示例如下:
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用foo(..) 时把this 绑定到obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
这些函数实际上就是通过`call(..)`或者`apply(..)`实现了 **显式绑定**,这样可少些一些代码。
4)new 绑定
在
JavaScript
中,构造函数 只是一些使用new
操作符时被调用的普通 函数。它们并不属于某个类,也不会实例化一个类。它们甚至都不能说是一种特殊的函数类型(new 的机制 实际上 和面向类的语言完全不同)。使用new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
- 创建(构造)一个全新的对象;
- 这个新对象会被执行[[ 原型]] 连接;
- 这个新对象会绑定到函数调用的this;
如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回该新对象。示例如下:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
当使用
new
来调用foo(..)
时,会构造出一个新对象并把它绑定到foo(..)
调用中的this
上。new
是最后一种可以 影响 函数调用时this
绑定行为的方法,称之为 new 绑定。
* **2.1.3 优先级**
理解了函数调用中`this`绑定的四条规则,需要做的就是找到函数的调用位置并判断应当应用哪条规则。
当某个调用位置可应用多条规则时,必须给这些规则设定 **优先级** 。
* **显式绑定**:优先级 > 隐式绑定;
* **new绑定**:优先级 > 隐式绑定;
* **隐式绑定**:优先级 < 显式绑定;
* **默认绑定**:优先级 最低 ;
* **判断 this**
可按照下面的顺序来进行判断,函数在某个调用位置应用的是哪条规则。
1. 函数是否在`new`中调用(new 绑定)?如果是,`this`绑定的则是新创建的对象。
var bar = new foo()
2. 函数是否通过`call`、`apply`(显式绑定)或硬绑定调用?如果是,`this`绑定的是指定的对象。
var bar = foo.call(obj2)
3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是,`this`绑定的是那个上下文对象。
var bar = obj1.foo()
4. 如果都不是,使用默认绑定。如果在严格模式下,就绑定到`undefined`,否则绑定到全局对象。
var bar = foo()
对于正常的函数调用来说,理解了这些就可以明白`this`的绑定原理了。
但是,凡事都有例外。
* **2.1.4 绑定例外**
某些场景下`this`的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是**默认绑定**规则。
**1)被忽略的`this`**
如果把`null`或者`undefined`作为`this`的绑定对象传入`call`、`apply` 或者`bind`,这些值在调用时会被忽略,其实际应用的是 默认绑定 规则。
示例如下:
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
什么情况下会传入null 呢?
常见的做法是使用`apply(..)`来“展开”一个数组,并当作参数传入一个函数。类似地,`bind(..)`可以对参数进行 柯里化(预先设置一些参数),该方法有时非常有用。如下所示:
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
这两种方法都需要传入`1`个参数当作`this`的绑定对象 。如果函数并不关心`this`,仍然需要传入一个占位值,这时`null`可能是一个不错的选择。
> 在ES6 中,可以用`...`操作符代替`apply(..)`来 “展开” 数组,`foo(...[1,2])`和`foo(1,2)`是一样的,这样可以避免不必要的`this`绑定。但是,在ES6中没有柯里化的相关语法,因此仍需使用`bind(..)`。
> 如果总是使用`null`来忽略`this`绑定可能产生一些副作用。如果某个函数确实使用了`this`(比如第三方库中的一个函数),那默认绑定规则会把`this`绑定到全局对象(在浏览器中这个对象是`window`),将导致不可预计的后果(比如修改全局对象)。因此,这种方式可能会导致许多难以分析和追踪的`bug`。
**2)更安全的this**
一种 “更安全” 的做法,是传入一个空的非委托对象,把this绑定到这个对象。
这样,任何对于`this`的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。由于这个对象完全是一个空对象,可以用`ø`来命名这个变量,它非常形象,比`null`的含义更清楚。
要在JavaScript中创建一个空对象,最简单的方法是:
Object.create(null)
`Object.create(null)`和`{}`很像, 但是并不会创建`Object.prototype`这个委托,所以它比`{}`“更空”:
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
**3)间接引用**
如果有意或无意地创建了一个函数的 “间接引用” 时,调用这个函数则会应用 **默认绑定** 规则。
间接引用最容易在 赋值时 发生:
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
默认绑定。赋值表达式`p.foo = o.foo`的返回值是目标函数的引用,因此调用位置是`foo()` 而不是`p.foo()`或者`o.foo()` 。
**注意**:对于 **默认绑定**,决定`this`绑定对象的,并不是 **调用位置** 是否处于严格模式,而是 **函数体** 是否处于严格模式。如果函数体处于严格模式,`this`会被绑定到`undefined`,否则`this`会被绑定到**全局对象**。
**4)软绑定**
硬绑定可把`this`强制绑定到指定的对象(除了使用`new`时),防止函数调用应用默认绑定规则。但是,硬绑定会大大降低函数的灵活性,使用硬绑定之后无法使用隐式绑定或显式绑定来修改`this`。
**软绑定方法**:
即给默认绑定指定一个全局对象和`undefined`以外的值,在实现和硬绑定相同的效果时,保留了隐式绑定或显式绑定修改`this`的能力。
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;
};
}
`softBind(..)`的其他原理和ES5 内置的`bind(..)`类似。—— 它会对指定的函数进行封装,首先检查调用时的`this`,如果`this`绑定到全局对象或者`undefined`,那就把指定的默认对象`obj`绑定到`this`,否则不会修改`this`。
**5)`softBind`实现软绑定**
示例代码:
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 <---- 应用了软绑定
`foo()`可以手动将`this`绑定到`obj2`或者`obj3`上,但如果应用默认绑定,则会将`this`绑定到`obj`。
* **2.1.5 `this` 词法**
前面介绍的四条规则包含所有正常的函数,但是, **箭头函数** 不使用 this 的`4`种标准规则,而是根据外层(函数或者全局)作用域来决定`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`也不行!)
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo() {
setTimeout(() => {
// 这里的this 在此法上继承自foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2
箭头函数可以像`bind(..)`一样确保函数的`this`被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的`this`机制。
在 ES6 之前也使用一种几乎和箭头函数完全一样的模式。示例如下:
function foo() {
var self = this;
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
`self = this`和箭头函数看起来都可以取代`bind(..)`,但从本质上讲,它们替代的是`this`机制。
如果你经常编写`this`风格的代码,但是绝大部分时候都会使用`self = this`或箭头函数来否定`this`机制,那你或许应当:
1. 只使用词法作用域并完全抛弃错误`this`风格的代码;
2. 完全采用`this`风格,在必要时使用`bind(..)`,尽量避免使用`self = this`和箭头函数。
> 包含这两种代码风格的程序可以正常运行,但是在同一个函数或者同一个程序中混合使用这两种风格会使代码更难维护,可能也会更难写。
--------------------
* **2.1.6 小结**
如果要判断一个运行中函数的`this`绑定,就需要找到这个函数的直接调用位置。找到之后 就可以顺序应用下面这四条规则来判断`this`的绑定对象。
1. 由new 调用?绑定到新创建的对象。
2. 由call 或者apply(或者bind)调用?绑定到指定的对象。
3. 由上下文对象调用?绑定到那个上下文对象。
4. 默认:在严格模式下绑定到undefined,否则绑定到全局对象。
一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略`this`绑定,你可以使用一个DMZ对象,比如`ø = Object.create(null)`,以保护全局对象。
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定`this`,具体来说,箭头函数会继承外层函数调用的`this`绑定(无论this 绑定到什么)。这其实和ES6之前代码中的`self = this`机制一样。
--------------------
3. 对象
我们知道函数调用位置的不同会造成this
绑定对象的不同。那,对象到底是什么,绑定它们的目的是什么?
3.1 语法
对象的定义形式有 2 种:
声明形式:
var myObj = {
key: value
// ...
}
构造形式:
var myObj = new Object();
myObj.key = value;
这两种形式生成的对象的 区别 是:
- 声明形式中,可以添加多个 键 / 值 对;
- 构造形式中,必须逐个添加属性。
一般来说,用“构造形式”创建对象使用较少,使用声明(文字)语法较常使用,包括绝大多数内置对象。
3.2 类型
在 JavaScript 中有 6
种主要数据类型:
string
number
boolean
null
undefined
object
除object
外,其余 5
种简单类型本身并不是对象,但 null
有时会被当做一种对象类型。这其实只是语言本身的一个bug
,即对null
执行typeof null
时,会返回字符串”object
“ 1。
函数 是对象的一个 子类型(从技术角度来说就是“可调用的对象”)。JavaScript 中的函数是“一等公民”,因为其本质上和普通的对象一样(只是可以调用),所以可以像操作其他对象一样操作函数(比如当作另一个函数的参数)。
数组 也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般的对象要稍微复杂。
对象子类型 - 内置对象
有些内置对象的名字看起来和简单基础类型一样,如下所示:
•
String
•Number
•Boolean
•Object
•Function
•Array
•Date
•RegExp
•Error
这些内置对象,实际上只是一些内置函数。可以被当作构造函数(由
new
产生的函数调用)来使用,从而可以构造一个对应子类型的新对象。示例如下:
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 检查sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]
这里可以简单地认为子类型在内部借用了
Object
中的toString()
方法。strObject
是由String
构造函数创建的一个对象。
类似于:
var strPrimitive = "I am a string";
~ 引擎会自动把字面量 `"I am a string"` ,转换成`String` 对象,使得我们可以访问其属性和方法:
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"
对于数值字面量,如果使用类似`42.359.toFixed(2)` 的方法(该方法可把 Number **四舍五入** 为指定小数位数的数字),引擎会把`42`转换成`new Number(42)`。对于*布尔字面量* 来说也是如此。
`null`和`undefined`没有对应的*构造形式*,只有*文字形式*。相反,`Date`只有*构造*,没有*文字*形式。
对于`Object`、`Array`、`Function`和`RegExp`(正则表达式)来说,无论使用*文字形式* 还是*构造形式*,它们都是**对象**,不是字面量。
> Error 对象很少在代码中显式创建,一般是在抛出异常时被自动创建。也可以使用new Error(…) 这种构造形式来创建,不过一般来说用不着。
3.3 对象内容 - 属性
属性访问 和 键访问
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2
.a
语法称为属性访问;["a"]
语法叫做键访问。
两种语法的 **主要区别**:
点(
.
)操作符要求属性名满足标识符的命名规范,而[".."]
语法可以接受任意UTF-8/Unicode
字符串作为属性名。【示例】:如果要引用名称为 “Super-Fun!” 的属性,那就必须使用
["Super-Fun!"]
语法访问,因为Super-Fun!并不是一个有效的标识符属性名。由于
[".."]
语法使用字符串来访问属性,所以可以在程序中构造这个字符串。示例如下:
var myObject = {
a:2
};
var idx;
if (wantA) {
idx = "a";
}
// 之后
console.log( myObject[idx] ); // 2
在对象中,属性名永远都是字符串。如果使用string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串(即使是数字也不例外)。
1)可计算属性名
如果需要通过表达式来计算属性名,那么
myObject[..]
这种属性访问语法,就可以派上用场了,如myObject[prefix + name]
。ES6 增加了可计算属性名,可在文字形式中使用
[]
包裹一个表达式来当作属性名:var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
这是一种新的基础数据类型,包含一个不透明且无法预测的字符串值。
2)属性与方法
有些函数具有
this
引用,有时候这些this
确实会指向调用位置的对象引用。但是这种用法从本质上来说并没有把一个函数变成一个“方法”;由于
this
是在运行时根据调用位置动态绑定的,所以 函数 和 对象 的关系最多也只能说是 间接关系 。示例:
function foo() {
console.log( "foo" );
}
var someFoo = foo; // 对foo 的变量引用
var myObject = {
someFoo: foo
};
foo; // function foo(){..}
someFoo; // function foo(){..}
myObject.someFoo; // function foo(){..}
someFoo
和myObject.someFoo
只是对于同一个函数的不同引用,并不能说明这个函数是特别的或者“属于”某个对象。严谨地说,“函数” 和 “方法” 在 JavaScript 中是可以互换的。
**注:** 即使在 **对象** 的文字形式中声明一个函数表达式,这个函数也不会“属于”该对象 —— 它们只是对于相同函数对象的多个引用,如下所示:
var myObject = {
foo: function() {
console.log( "foo" );
}
};
var someFoo = myObject.foo;
someFoo; // function foo(){..}
myObject.foo; // function foo(){..}
3)数组
数组也支持
[]
访问形式,不过数组有一套更加结构化的值存储机制。数组期望的是数值下标,也就是说值存储的位置(通常被称为 索引 )是整数,比如说0
和42
:var myArray = [ "foo", 42, "bar" ];
myArray.length; // 3
myArray[0]; // "foo"
myArray[2]; // "bar"
**数组** 也是 **对象** ,虽然每个下标都是整数,但仍然可以 **添加属性**。
示例:
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
这里虽然添加了命名属性(无论是通过. 语法还是`[]`语法),数组的`length`值并未发生变化。
4)复制对象
思考一个对象:
function anotherFunction() { /*..*/ }
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是复本!
c: anotherArray, // 另一个引用!
d: anotherFunction
};
anotherArray.push( anotherObject, myObject );
如何准确地表示myObject 的复制?
首先,应该判断是浅复制还是深复制。
* 对于 **浅拷贝**,复制出的新对象中`a`的值会复制旧对象中`a`的值(`2`),但是新对象中b、c、d 三个属性其实只是三个引用,和旧对象中b、c、d 引用的对象是一样的。
* 对于 **深拷贝**,除了复制`myObject`以外,还会复制`anotherObject`和`anotherArray`。问题就来了,`anotherArray`引用了`anotherObject`和`myObject`,所以又需要复制`myObject`,就会由于循环引用导致 **死循环**。
我们是应该检测循环引用、并终止循环(不复制深层元素)?还是应当直接报错或是选择其他方法?
对于JSON 安全(也就是说可以被序列化为一个JSON 字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );
但是,这种方法需要保证对象是JSON 安全的,所以只适用于部分情况。
相比深拷贝,浅拷贝易懂、并且问题要少得多。所以 ES6 定义了`Object.assign(..)`方法来实现浅拷贝。
`Object.assign(..)`方法的第`1`个参数是目标对象,之后还可以跟`1`个或多个源对象。它会遍历`1`个或多个源对象的所有可枚举的自有键,并把它们复制(使用= 操作符赋值)到目标对象,最后返回目标对象,示例如下:
var newObj = Object.assign( { }, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
> 由于Object.assign(…) 就是使用= 操作符来赋值,所以源对象属性的一些特性(比如writable)不会被复制到目标对象。
~ 未完待续~
脚注:
null
执行typeof null
时,会返回字符串”object
“原理是:
因不同的对象在底层都表示为二进制,而在JavaScript中,如二进制前三位都为0
,则会被判断为object
类型,null
的二进制表示是全0
,自然前三位也是0
,所以执行typeof
时会返回“object
”。 ↩︎
还没有评论,来说两句吧...