你不知道的 JavaScript 上卷 第二部分 笔记

501 阅读14分钟

第1章 关于 this

1.1 为什么要用 this

this提供了一种更优雅的方式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。

var obj = {
    name: 'Reader',
    speak: function() {
        console.log(this.name);
    }
};

obj.speak(); // Reader

1.2 误解

有两种对this常见的误解:

  1. 指向自身
  2. this的作用域
// 第一个误解: this 指向自身
function foo(){
    console.log(this.count);
}

foo.count = 4;
var count = 3;
foo();

this并不指向foo函数,而是查找外层作用域,最终找到全局作用域的count

// 第二个误解:this 指向函数的作用域
function foo() {
    var a = 2;
    this.bar();
}

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

foo(); // undefined

首先,foo函数向外层作用域找到bar函数,然后逐层向外找a,到全局作用域找到window对象,然后window上没有a属性,所以是undfined

注意:this在任何情况都不指向函数的词法作用域。在 JavaScript 内部,作用域和对象很相似,但是作用域无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。

1.3 this 到底是什么

this是在运行时绑定的,不是在编写时绑定,它取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(也称执行上下文)。这个记录包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数信息。this就是这个记录的一个属性,会在函数执行过程中用到。

第2章 this 全面解析

2.1 调用位置

分析调用位置最重要的是分析调用栈。

动图来自前端开发都应该懂的事件循环(event loop)以及异步执行顺序(setTimeout、promise和async/await)

2.2 绑定规则

2.2.1 默认绑定

当没有其他绑定时,使用默认绑定,非严格模式下,this绑定到全局作用域下的全局对象window;严格模式下,不能使用全局对象,因此this会绑定到undefined

function foo() {
    console.log(this.a);
}

var a = 1;

foo(); // 1

2.2.2 隐式绑定

查看调用位置是否有上下文对象,或者说是否被某个拥有或者包含,如果是,this“相当于”那个对象的引用。

function foo() {
    console.log(this.a)
}

var a = 1;

var obj = {
    foo: foo,
    a: 2
}

obj.foo(); // 2
foo(); // 1
var foo1 = obj.foo;
foo1(); // 1
1





undefined

此时将objfoo方法赋给foo1, 此时调用foo1相当于直接调用foo

2.2.3 显式绑定

如果我们想使用一个对象上的方法,并在某个对象上使用,这个时候就需要显式绑定,用到call方法和apply方法。
具体用法:Function.prototype.applyFunction.prototype.call

1. 硬绑定

硬绑定是一种非常常用的模式,所以ES5提供了内置的方法Function.prototype.bind

// TODO: 理解更为复杂的bind的实现
// Function.prototype.bind 的 Polyfill(来自MDN)
// Does not work with `new funcA.bind(thisArg, args)`
if (!Function.prototype.bind) (function(){
  var slice = Array.prototype.slice;
  Function.prototype.bind = function() {
    var thatFunc = this, thatArg = arguments[0];
    var args = slice.call(arguments, 1);
    if (typeof thatFunc !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - ' +
             'what is trying to be bound is not callable');
    }
    return function(){
      var funcArgs = args.concat(slice.call(arguments))
      return thatFunc.apply(thatArg, funcArgs);
    };
  };
})();
2. API 调用“上下文”

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选参数,通常被称为“上下文”,其作用和bind一样。

function foo(el) {
    console.log(el, this.id);
}

var obj = {
    id: 'awesome'
};

[1, 2, 3].forEach(foo, obj);
// 1 'awesome'
// 2 'awesome'
// 3 'awesome'

2.2.4 new 绑定

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建一个全新的对象
  2. 这个新对象会执行[[Prototype]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个对象

2.3 优先级

new > 显式绑定 > 隐式绑定 > 默认绑定

显然,隐式绑定优先级大于默认绑定,因为如果默认绑定优先级大于隐式绑定,则通过对象调用方法时会绑定全局对象而不是绑定该对象。

function foo() {
    console.log(this.a)
}

var obj = {
    foo: foo,
    a: 2
}

var obj1 = {
    a: 3
}

obj.foo.call(obj1); // 3

上面的例子说明,显式调用优先级大于隐式调用。最后我们需要判定new与显式绑定和隐式绑定的优先级。

function foo(something){
    this.a = something;
}

var obj1 = {
    foo: foo
};
obj1.foo(2);

var obj2 = new obj1.foo(4);

console.log(obj1.a); // 2
console.log(obj2.a); // 4

可以看到new绑定比隐式绑定优先级要高。

注意:newcall/apply无法一起使用,因此无法直接测试,但是可以通过硬绑定测试。

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

bar硬绑定在obj1上,但是new bar(e)并没有修改obj1,而是绑定到一个新的对象。

2.4 绑定例外

2.4.1 被忽略的this

如果把null或者undefined作为绑定对象传给call/apply或者bind,这些值在调用是会被忽略,实际应用的是默认绑定规则。
一种常见做法是使用apply来“展开”一个数组,当作参数传入一个函数。类似的,bind可以对参数柯里化,这种方法有时非常有用。

function foo(a, b) {
    console.log('a:', a, 'b:', b);
}

foo.apply(null, [2, 3]); // a: 2 b: 3
var bar = foo.bind(null, 2);
bar(3); // a: 2 b: 3

总是使用null作为绑定对象,会有一些潜在的副作用,如果某个函数使用了this,那么默认绑定规则会把this绑定到全局对象,这会导致不可预测的结果。

更安全的this

更安全的办法是传入一个特殊的对象,把this绑定到这个对象不会有任何副作用。
我们可以在忽略this绑定时传入一个空对象,不会对全局对象产生影响。在 JavaScript 中创建一个空对象最简单的方法是Object.create(null)Object.create(null)null很像,但是它并不会创建Object.prototype这个委托,所以它更“空”。

function foo(a, b) {
    console.log('a:', a, 'b:', b);
}

var empty = Object.create(null);

foo.apply(empty, [2, 3]); // a: 2 b: 3

2.4.2 间接引用

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...)返回的是foo函数,所以会使用全局对象的a

2.4.3 软绑定

// TODO:软绑定的实现

2.5 this词法

箭头函数不使用this的四种标准,而是根据外层作用域来确定this

function foo() {
    return (a) => {
        conosle.log(this.a)
    }
};

var obj1 = {
    a: 2
};

var obj2 = {
    a: 3
}

var bar = foo.call(obj1);
bar.call(obj2); // 2 而不是3

foo.call(obj1);运行完毕时,箭头函数的this已经绑定在obj1上,已经无法通过硬绑定重新绑定。

第3章 对象

3.1 语法

var myObject = {
    key: 'value'
}
// 或
var myObj = new Object();
myObject.key = 'value';

3.2 类型

在JavaScript中有六种主要类型:

  • string
  • number
  • boolean
  • null
  • undefined
  • object

(ES6中,新加了symbol

简单基本类型(string、boolean、number、null 和 undefined)本身并不是对象。null有时会被当成一种对象类型,但是这是语言本身的一个bug,实际上,null本身是基本类型。

内置对象

JavaScript中还有一些对象子类型,通常被称为内置对象。有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂。

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

在JavaScript中,他们实际上是一些内置函数,这些函数可以当作构造函数来使用,从而可以构造一个对应子类型的新对象。

var strPrimitive = "I'm a string";
typeof strPrimitive; // string
strPrimitive instanceof String; // false

var strObject = new String("I'm a string");
typeof strObject; // 'object'
strObject instanceof String; // true

**注意:原始值I'm a string不是一个对象,他只是一个字面量,并且不是一个不可变的值。如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要转换为String对象,幸好在必要时语言会自动把字符串字面量转换成一个String对象。**number存在类似行为。

nullundefined没有对应的构造式,只有文字形式。相反Date只有构造,没有文字形式。

对于ObjectArrayFunctionRegExp来说,无论使用文字形式还是构造形式,它们都是对象。
Error对象很少在代码中显式创建,一般是在抛出异常时被自动创建。

3.3 内容

键访问obj['key'],属性访问obj.key

3.3.1 可计算属性名

通过表达式来计算属性名,可以使用obj[perfix+name]使用。

3.3.2 属性与方法

3.3.3 数组

数组也是对象,虽然每个下标都是整数,仍然可以给数组添加属性。

3.3.4 复制对象

对象拷贝分为深拷贝和浅拷贝,浅拷贝其实只是对原有对象的引用,原对象发生改变则浅拷贝的对象也会发生变化。

var obj = {
    a: 1,
    b: 2,
    c: 3
}

var obj1 = obj;
obj1.a; // 1
obj.a = 2;
obj1.a; // 2

深拷贝比浅拷贝麻烦得多,JavaScript有一种办法实现深拷贝。

var obj = {a:3};
var newObj = JSON.parse(JSON.stringify(obj));
newObj.a; // 3
obj.a = 2;
newObj.a; // 3

但是这种方法的前提是保证对象是JSON安全的,所以只适用部分情况。

尽管,JavaScript的Object上有assign方法,他可以进行对象间的复制,但是仍然不满足深拷贝的要求。
Object.assign的详细信息:Object.assign() - JavaScript | MDN

var obj = {
    a: 1,
    b: {
        c: 3
    }
}

var newObj = Object.assign({}, obj);
obj.a = 2
newObj.a // 1
obj.b.c = 4;
newObj.b.c; // 4

尽管,复制出了一个newObj,但是它内部的b属性还是引用的obj内部的b属性,还是浅拷贝。

3.3.5 属性描述符

Object.getOwnPropertyDescriptor(obj, prop)获取属性描述符。
Object.defineProperty(obj, prop, descriptor)添加一个新属性或者修改一个已有属性。

3.3.6 不变性

1.对象常量
结合writeable: falseconfigurable:false就可以创建一个真正的常量属性(不可被修改、重定义或者删除)
2.禁止扩展
Object.preventExtensions(obj)禁止一个对象添加新属性并且保留已有属性。
3.密封
Object.seal在禁止扩展基础上,把现有属性标记为configurable:false
4.冻结
Object.freeze在密封的基础上,把所有属性标记为writable:false

**注意:**这些功能只能作用在一个对象的键上,但是如果某一个键的值是一个对象,该对象不会受到影响,即嵌套对象内部的对象的可变性不受影响。如果像深度修改,逐级遍历内部的对象。

3.3.7 [[Get]]

var myObject = {
    a: 2
};

myObject.a;

在语言规范中,myObject.amyOjbect上实际上实现了[[Get]]操作,首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性,否则就会在原型链上寻找。
TODO:原型链见后文

3.3.8 [[Put]]

有获取属性的操作,自然有对应的[[Put]]操作,[[Put]]算法大致会检查下面这些内容:

  1. 属性是否是访问描述符(参见3.3.9节)?如果是并且存在setter就调用setter
  2. 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在严格模式下抛出TypeError异常
  3. 如果不是,将该值设置为属性的值

如果对象中不存在这个属性,[[Put]]操作会更加复杂,TODO:在后文讨论。

3.3.9 Getter和Setter

在ES5中可以使用gettersetter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。

var myObject = {
    get a() {
        console.log('this is getter');
        return 2;
    }
}
myObject.a; 
// this is getter
// 2
Object.defineProperty(
    myObject,
    "b",
    {
        get: function(){ return this.a * 2 },
        enumerable: true,
        configurable: true
    }
);
myObject.b;
// this is getter
// 4

为了让属性更合理,还应当定义setter,和你期望的一样,setter会覆盖单个属性默认的[[Put]]操作。通常来说gettersetter是成对出现的(只定义一个的话通常会产生意料之外的行为):

var myObject = {
    get a() {
        return this._a_;
    },
    set a(val) {
        this._a_ = val * 2;
    }
}

myObject.a = 2;
myObject.a; // 4

3.3.10 存在性

访问一个对象个属性返回为undefined,但是这个属性是不存在,还是存在但是值是undefined,如何区分这两种情况?

我们可以在不访问属性值情况下判断对象中是否存在这个属性:

var myObject = {
    a: 2
};

"a" in myObject; // true
"b" in myObject; // false

myObject.hasOwnProperty('a'); // true
myObject.hasOwnProperty('b'); // false

in操作符会检查属性是否咋爱对象及其[[Prototype]]原型链中。TODO:参见第5章。相比之下,hasOwnProperty只会检查属性是否在myObject中,不会检查原型链。

Object.keys()返回对象直接包含的所有可枚举属性,Object.getOwnPropertyNames()返回对象直接包含的所有属性,无论是否可枚举。

3.4 遍历

for...in循环可以用来遍历对象的可枚举属性列表(包括原型链)。
for(var i = 0; i< len; i++){...}的方式其实不是在遍历值,而是用下标来指向值然后访问。
forEach会遍历数组所有值,并返回回调函数的返回值。
every一直运行回调函数返回fasle
some一直运行到回调函数返回真值

这部分省略for..of的内容,这部分直接看ES6的教程更好些,后面会省略部分ES6的内容。

第4章 混合对象“类”

类/继承描述了一种代码组织结构形式——一种对真实世界中问题领域的建模方法。

举个例子,以汽车为例:

就是图纸,图纸上包含了它的各种零件以及它具备什么样的功能。

实例化就是按照图纸造一辆车出来。

// 伪代码
calss Vehicle{
    engines = 1
    
    run() {
        console.log('run')
    }
    
    toot() {
        console.log('toot')
    }
}

类的实例化是由一个特殊的方法来构造的,这个方法被称为构造函数,通过new来调用。别的语言中,这个方法名和类名相同。JavaScript比较特殊,后面会说明。

类一个很重要的特性就是继承,假设有一个父类,一个子类,子类继承父类,父类的特性会复制给子类。
用车辆举例,交通工具是父类,小轿车是子类,它继承了交通工具的所有特性。
类另一个重要的特性是多态,用交通工具举例,交通工具和小轿车都可以驾驶,但是小轿车是四轮驱动的行驶,小轿车的类定义了自己行驶方法,行驶时会使用自身的行驶方法,而不是父类的。

calss Car inherits Vehicle{
    run() {
        console.log('car run')
    }
}

多重继承就是继承多个父类,但是JavaScript本身不提供多重继承。

4.4 混入

在JavaScript中,只存在对象,不存在类,在其他语言中类表现出来的都是复制行为,因此JavaScript开发者也想出一个方法来模拟类的复制行为,这个方法就是混入。

第5章 原型

5.1 [[Prototype]]

JavaScript 中又一个特殊的[[Prototype]]内置属性,其实就是对其他对象的饮用。几乎所有的对象在创建时[[Prototype]]的属性都会被赋予一个非空的值。
然而虽然说[[Prototype]]是一个隐藏属性,但很多浏览器都给每一个对象提供__proto__这一属性,这个属性就是上文反复提到的该对象的[[prototype]]。由于这个属性不标准,因此一般不提倡使用。

在第3章中说过,当试图引用对象的属性时会触发[[Get]]操作,比如myObject.a,对于默认的[[Get]]操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。
但是如果不存在,就需要使用对象的[[Prototype]]链了。for...in同理。

var obj = {
    a: '1'
};

var source = {
    b: 'source: 2'
};

obj.__proto__ = source; // 不推荐这么使用
obj.b; // source: 2
source.b = 'source: 22';
obj.b; // source: 22
'source: 22'
var source = {
    b: 'source: 2'
};

var obj = Object.create(source);
obj.b; // source: 2

5.1.1 Object.prototype

哪里是[[Prototype]]链的“尽头”?
所有普通的[[Prototype]]最终都会指向内置的Object.prototype

5.1.2 属性的设置和屏蔽

属性访问时,如果不直接存在对象上时,会遍历原型链。如果,在原型链上遍历时,先发现的对象上有该属性,就取出该属性,不再查找后面的对象,就屏蔽了原型链后面的同名属性。

但是在设置属性时,会有出人意料的行为发生:

  1. 如果在原型链上存在该属性并且没有被标记为只读,那就直接在对象上添加该属性。
  2. 如果在原型链上存在该属性但被标记为只读,那么无法修改已有属性和在对象上添加新的属性。严格模式下,会抛出一个错误;否则这条语句会被忽略。
  3. 如果在原型链上存在该属性并且它是一个setter,那就会调用这个setter,不会被添加到该对象,也不会在对象上重新定义一个setter

5.2 “类”

在JavaScript中,只有对象。

总结到 JavaScript 类这一块儿的时候,书里有很多理论性的内容,这部分需要单独总结,而且工作量还不小,所以等后面有时间了,会尝试总结一下。