你不知道的Javascript(上卷)笔记

词法作用域

词法作用域是由写代码时将变量和块作用域写在哪决定的。无论函数在哪里被调用,也无论何时被调用,它的词法作用域都只由函数被声明时所处的位置决定。
可以在运行时来”修改”欺骗词法作用域,Javascript 有两种机制来实现:eval()和width(),但欺骗词法作用域会导致性能下降,不建议这么做。

1
2
3
4
5
6
7
8
9
function foo() {
console.log(a); //2
}
function bar() {
var a = 3;
foo()
}
var a = 2;
bar();

Javascript 并不具有动态作用域,只有词法作用域。
主要区别就是:词法作用域是在写代码或者定义时确定,动态作用域在运行时确定(this也是),词法作用域关注声明在何处,动态作用域关注函数从何处调用。

函数作用域

函数声明和函数表达式的一个重要区别就是:它们的名称标识符将被绑定在何处。

1
2
3
4
5
6
7
8
9
// 函数声明
// foo 被绑定在所在作用域中
var a = 2;
function foo() {
var a = 3;
console.log(a)
}
foo();
console.log(a);

1
2
3
4
5
6
7
8
// 函数表达式
// foo 被绑定在函数表达式自身的函数中,不会污染外层作用域
var a = 2;
(function foo() {
var a = 3;
console.log(a)
})()
console.log(a);

(function foo() {…})是立即执行表达式,第一个()将函数变成表达式,第二个()执行了这个函数。改进的立即执行表达式:(function(){…}())。
var 声明包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2: 会被看成两个声明:var a 和 a = 2;第一个声明是在编译阶段进行,第二个声明会被留在原地等待执行。
函数声明会被提升,函数表达式不会。

1
2
3
4
5
foo() // 不是 ReferenceError, 而是 TypeError。
// 因为foo()被提升并分配给所在的作用域,因此foo()不会导致ReferenceError。
// 但是foo此时没有赋值,foo()由于对undefined值进行函数调用导致非法操作,所以抛出TypeError异常

var foo = function bar() {}

块作用域

Javascript 没有块作用域的概念。
es6 引入 let 关键字,可以将变量绑定在所在的任意作用域中(通常是{}内部)。let进行的声明不会在块作用域中进行提升。

1
2
console.log(bar) // ReferenceError!
let bar = 2

作用域与闭包

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

循环与闭包:

1
2
3
4
5
6
for(var i = 0; i <=5; i++) {
setTimeout(function(){
console.log(i)
}, 0)
}
// 6 6 6 6 6 6

要打印出0 1 2 3 4 5需要改成:

1
2
3
4
5
6
7
for(var i = 0; i <=5; i++) {
(function(j) {
setTimeout(function(){
console.log(j)
}, 0)
})(i)
}

因为IIFE(立即执行函数)会通过声明并立即执行一个函数来创建作用域
在迭代内部使用IIFE会为每个迭代内部生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

关于 this

第一种误解:this 指向函数本身。
第二种误解:this 指向函数的作用域。
正确的是:this 是在运行时绑定的,并不是在编写时绑定,它的上下文只取决于函数调用时的各种条件,它的指向完全取决于函数在哪里被调用。

绑定规则

  1. 默认绑定:独立函数调用
    • 非严格模式:this 绑定到全局
    • 严格模式:this 被绑定到 undefined
  2. 隐式绑定:
    函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
    有个隐式丢失的问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function foo() {
    console.log(this.a)
    }
    var obj = {
    a: 2,
    foo: foo
    }
    var bar = obj.foo; // 函数别名!
    var a = "oops, global"
    bar(); // oops, global

    bar 是 obj.foo 的引用,但实际上引用的是 foo 函数本身,所以是默认绑定

  3. 显示绑定
    call(…) 和apply(…) 方法

  4. new 绑定
    new 调用函数时,会自动执行以下操作:

    • 创建(或者说构造)一个全新的对象。
    • 这个新对象会被执行[[prototype]]连接。
    • 这个新对象会绑定到函数调用的 this。
    • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

判断 this

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或者 apply(或者)bind 调用? 绑定到指定对象。
  3. 由上下文对象调用? 绑定到那个上下文对象。
  4. 默认:严格模式下绑定到 undefined,否则绑定到全局对象。

绑定例外

this 词法
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或全局)作用域来决定 this。
理解箭头函数的词法作用域,箭头函数的绑定无法被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
return (a)=>{
// this 继承自 foo(),取决于调用时foo() 的 this。
console.log(this.a)
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1);
bar.call(obj2); // 2

函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
setTimeout(() => {
// 箭头函数的定义生效是在foo函数生成时。如果是普通函数,这是21
console.log('id:', this.id);
}, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

对象

内置对象:
String、Number、Boolean、Object、Function、Array、Date、RegExp、Error

1
2
3
4
5
6
7
8
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
Object.prototype.toString.call(strObject); // [object String]

Object.prototype.toString是子类型在内部借用了 Object 的 toString() 方法。

1
2
3
var strPrimitive = "I am a string";
strPrimitive.length // 13
strPrimitive.charAt(3) // "m"

strPrimitive 是一个字面量,能调用 .length 等方法,是因为引擎自动将字面量转化为 String 对象。

在对象中,属性名永远是字符串,如果用非 string 以外的值作为属性名,会首先把它转换为一个字符串。
可以给数组添加属性

1
2
3
4
var myArray = ["foo", 42, "bar"];
myArray.baz = "baz";
myArray.length //3
myArray.baz // baz

虽然添加了属性,但是长度 length 并没有发生变化。

1
2
3
4
var myArray = ["foo", 42, "bar"];
myArray["3"] = "baz";
myArray.length //4
myArray[3] // baz

当属性名”看起来”像是一个数字,就会变成一个数值下标。

混合对象”类”

原型

[[prototype]]

javascript 中有一个特殊的[[prototype]]内置属性,其实是对于其他对象的引用,几乎所有对象在创建时[[Prototype]]属性都会被赋予一个非空的值,在进行属性查找的时候,一直会沿着这条链进行查找。

Javascript 中只要有对象,实际上,Javascript 才是真正应该被称为”面向对象”的语言,因为它是少有的可以不通过类,直接创建对象的语言。在Javascript中,类无法描述对象的行为,(因为根本不存在类!)对象直接定义自己的行为。Javascript 中只有对象

在面向类的语言中,类可以被复制(或者说)实例化多次,意味着”把类的行为复制到物理对象中”。
但是在 Javascript 中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,他们的 [[prototype]] 关联的是同一个对象,但是在默认情况下并不会进行复制,因此这些对象直接并不会完全失去联系,它们是互相关联的。

new Foo()会生成一个新对象(我们称之为a),这个新对象的内部链接 [[prototype]] 关联的是 Foo.prototype 对象。最后我们得到了两个对象,他们之间互相关联

继承意味着复制操作,Javascript 默认不会复制对象属性,Javascript 会在两个对象之间创建关联,这样一个对象可以通过委托访问另一个对象的属性和函数

1
2
3
4
5
function Foo() {};
Foo.prototype = {};
var foo = new Foo();
foo.constructor === Foo // false
foo.constructor === Object // true

首先 foo 对象上并没有属性 constructor,foo.constructor是通过默认的[[prototype]]委托指向Foo,因为现在改写了Foo.prototype值,所以Foo.prototype上也没有了默认属性.constructor,所以会顺着原型链接着找,直到找到顶端 Object.prototype
foo.constructor 是一个非常不可靠并且不安全的引用,通常来说要避免这些引用。

原型继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name
}
function Bar(name, label) {

Foo.call(this, name);
this.label = label;
}
// 创建了一个新的Bar.prototype对象并关联到Foo.prototype,但同时会丢失contructor属性。
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.myLabel = function() {
return this.label;
}
var a = new Bar("a","obj a");
a.myName(); // "a"
a.myLabel(); // "obj a"

下面有两种常见的错误做法

1
2
3
4
5
// 直接将Bar.prototype指向Foo.prototype对象,这样如果修改某个prototype值也会修改另一个
// 并不会创建一个关联到Bar.prototype的新对象
Bar.prototype = Foo.prototype;
// 使用Foo()构造函数调用创建关联,如果Foo函数本身有副作用会影响到Bar()的后代。
Bar.prototype = new Foo();

ES6 提供了辅助函数Object.setPrototypeOf()来修改对象的[[prototype]]关联。

1
2
3
4
// ES6 之前 会抛弃默认的Bar.prototype,需要对抛弃的对象进行垃圾回收
Bar.prototype = Object.create(Foo.prototype);
// ES6 可以直接修改现有的Bar.prototype
object.setPrototypeOf(Bar.prototype, Foo.prototype);

检查”类”关系

检查一个实例(Javascript中的对象)的继承祖先(Javascript中的委托关联)通常称为内省(或者反射)。
第一种: a instanceof b
instanceof 只能判断对象和函数(带.protptype引用的Foo)之间的关系。如果你想判断两个对象直接是否通过 [[prototype]] 链关联,只用 instanceOf 无法实现。
第二种: Foo.prototype.isPrototypeOf(a)
isPrototypeOf() 回答的是: 在a的整条 [[prototype]] 链中是否出现过 Foo.prototype
和 instanceof 类似,但是 isPrototypeOf 并不需要间接引用函数(Foo), 它的 .prototype 属性会被自动访问。
也就是说,只需要两个对象即可判断它们之间的关系,

1
2
// b 是否出现在 c 的 [[prototype]]链中?
b.isPrototypeOf(c)

直接获取一个对象的 [[prototype]] 链: Object.getPrototypeOf(a)

对象关联

var bar = Object.create(foo);
Object.create(…) 是在 ES5 中新增的函数,在 ES5 之前的环境如果要支持的话就需要使用一段简单的polyfill代码。

1
2
3
4
5
6
7
if(!Object.create) {
Object.create = function(o) {
function F() {};
F.prototype = o;
return new F();
}
}

行为委托

// TODO