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

第1章 关于 this

1.1 为何要用 this

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

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函数,而是查找外层做用域,最终找到全局做用域的countjava

// 第二个误解: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就是这个记录的一个属性,会在函数执行过程当中用到。promise

第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“至关于”那个对象的引用。app

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 类这一起的时候,书里有不少理论性的内容,这部分须要单独总结,并且工做量还不小,因此等后面有时间了,会尝试总结一下。

相关文章
相关标签/搜索