精读《你不知道的JavaScript(上)》三

96 阅读14分钟

第二部分

第一章 关于this

1.1 为什么要用this

this提供了一种更优雅的方式隐式的传递一个对象引用,从而使API设计更加的简洁,并且易于复用

function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READER
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify( you ); // READER
speak( me ); //hello, 我是 KYLE

1.2 误解

1.2.1 误解一:指向自身

按照this单词的语意,我们总是会把它认为是指向自身,事实上有时确实如此,但是并不总是指向自身

function foo(num) {
    //记录count被调用的次数
    this.count++;//this指向全局作用域winodw
}
foo.count = 0;
var i;
for(i=0;i<10;i++){
    if(i>5){
        foo(i);
    }
}
//foo:6
//foo:7
//foo:8
//foo:9
console.log(foo.count);//0

1.2.2 误解二:指向它的作用域

首先this有时候指向作用域,有时候又不是,但是明确的一点就是任何时候this都是不会指向他的词法作用域。因为词法作用域是属于引擎的,无法通过js代码进行访问。

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log(this.a);//虽然是在foo函数中调用的,但是this指全局作用域window
}
foo();//undefined

切记: this 在任何情况下都不指向函数的词法作用域。你不能使用 this 来引用一个词法作用域内部的东西。

1.3 this到底是什么

this 是在运行时绑定的,并不是在编写时绑定,它的上下文(对象)取决于函数调用时的各种条件。

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到

1.4 小结

  • 学习 this 的第一步是明白 this 既不指向函数自身也不指向函数的词法作用域
  • this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

第二章 this全面解析

2.1 调用位置

调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

function foo() {
    function bar() {
   }
   bar();//在foo中声明,所以bar的调用位置在foo中
}
foo();//在全局中声明,所以foo内的调用位置在window上

上面的代码中分析调用栈只是为了在运行时找到我们关心的函数到底在哪里和被谁调用了。 但是实际上写代码时,我们只需要记住this的指向就是我们调用该函数的上下文对象,即我们在哪里调用该函数,this就指向哪里。(查看调用栈还可以通过浏览器的开发者工具,只需在疑惑的代码上一行加上debugger即可)

2.2 绑定规则

2.2.1 默认绑定 当被用作独立函数调用时(不论这个函数在哪被调用,不管全局还是其他函数内),this默认指向到window;(如果使用严格模式,那么全局对象将无法使用默认绑定,this 会绑定undefined上)

比如前面2.1 调用位置中的bar函数,尽管是在foo中调用,但函数是光秃秃的直接调用的,所以bar中this指向window

2.2.2 隐式绑定

隐式绑定: 函数被某个对象拥有或者包含,也就是函数被作为对象的属性所引用.例如obj.func(),此时this会绑定到该对象上.
隐式丢失: 不管是通过函数别名或是将函数作为入参造成的隐式丢失,它真正的调用位置,函数前没有任何修饰也没有显式绑定,那么this则会进行默认绑定,指向window.

var a = 3;//我是全局的3
function foo(){
    console.log(this.a);
}
var obj = {
    a:2,//我是对象的2
    foo:foo
};

obj.foo();//2,this绑定的是obj(隐式绑定)
obj["foo"]();//2,this绑定的是obj(隐式绑定)

var bar = obj.foo;//注意,这里不是调用,后面没有(),只是引用
bar();//结果是3,明明bar就是foo函数,bar的调用位置是光秃秃的直接调用,所以这里this绑定到window上(隐式丢失)

2.2.3 显式绑定

this绑定到某个对象上,但是不会产生隐式丢失,就是指直接使用call()、apply()、bind()

function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
};
foo.call(obj);//2。也就是把foo函数的this,绑定到obj上

2.2.4 new绑定

在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数,它们只是被 new 操作符调用的普通函数而已。

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

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

示例:

function foo(a) { 
  this.a = a;
}
var bar = new foo(2); 
console.log( bar.a ); // 2

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。

2.3 优先级

new绑定>显示绑定>隐式绑定>默认绑定
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则,可以按照下面的顺序来进行:

  • 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,否则绑定到全局对象。

2.4 绑定例外

1.被忽略的this 如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则

什么情况下你会传入 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

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 = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。

2.5 this词法

ES6 中介绍了一种无法使用上面四条规则的特殊函数类型:箭头函数。
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。

重要:

  • 箭头函数最常用于回调函数中,例如事件处理器或者定时器.
  • 箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象
  • 箭头函数用更常见的词法作用域取代了传统的 this 机制。
var a = 3;
var obj = {
    a:2
};
function foo(){
   ((a)=>{
        //this继承foo
        console.log(this.a);
    })()
}
foo()//3 这里光秃秃调用,foo中this是window,所以回调中是window的

注意: 下面这种情况:

function module() {
  return this.x;
}
var foo = {
  x: 99,
  bar:module.bind(this) //此时bind绑定的this为window.
}
var x="window"
console.log(foo.bar())//window
var a = 3;
var obj = {
    a:2
};
function foo(){
   ((a)=>{
        //this继承foo
        console.log(this.a);
    })()
}
foo.call(obj)//2 这里显示调用,foo中this是obj,所以回调中是obj的a

第三章 对象

3.1 语法

对象可以通过两种形式定义:声明(对象字面量)形式和构造形式。

  • 声明形式(对象字面量):
var myObj = { 
  key: value
  // ... 
};
  • 构造形式:
var myObj = new Object(); 
myObj.key = value;

3.2 类型

对象是 JavaScript 的基础。在 JavaScript 中一共有六种主要类型

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

简单数据类型:
其中string、boolean、number、null 和 undefined属于简单基本类型,并不属于对象.

对象:
对象除了我们自己手动创建的,JavaScript其实内置了很多对象
内置对象:

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

在 JavaScript 中,这些内置对象实际上只是一些内置函数。这些内置函数可以当作构造函数来使用,如var str = new String('str')

3.3 内容

对象属性: 由一些存储在特定命名位置的值组成.
属性名: 存储在对象容器内部的属性的名称,属性值并不会存在对象内,而是通过属性名来指向这些值真正的存储位置
属性名的两种形式:

  1. 使用.操作符.也是我们最常用的形式,被称为"属性访问"
  2. 使用[".."] 语法进行访问,被称为"键访问"

注意: 在对象中,属性名永远都是字符串,即使是数字也不例外

3.3.1 可计算属性名

ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名:

var prefix = "foo";

var myObject = {
   [prefix + "bar"]:"hello", 
   [prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world

const prefix = 'foo'

const obj = {
  [`${prefix}bar`]: 1,
  [`${prefix}baz`]: 2
}

obj.foobar     //  1
obj['foobar']  //  1
obj['foobaz']  //  2

3.3.2 属性与方法

  • 实际上对象内部引用的函数并不属于该对象,它不过是对函数的引用,对象属性访问返回的函数和其他函数没有任何区别(除了可能发生的隐式绑定this到该对象)。

3.3.3 数组

  • 数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性.例如myArray.baz = "baz"。注意:添加新属性后,虽然可以访问,但数组的 length 值不会改变

3.3.4 复制对象-浅拷贝和深拷贝

浅拷贝:JavaScript 引擎为了避免一次性复制过大的对象副本, 在对对象进行赋值时, 只是引用了目标对象的内存地址, 并没有创建全新的副本

var foo = {
    name: 'muzi'
}

var bar = { foo: foo }

bar.foo.name = 'yaya'
console.log(foo.name)  // yaya

foo.name = 'dundun'
console.log(bar.foo.name)  // dundun

ES6 定义了 Object.assign 方法来实现浅拷贝, 该方法的第一个参数是目标对象, 之后可以跟多个源对象,但它只复制对象的第一层属性,如下所示

var foo = {
    name: 'muzi',
    info: {
        age: 25,
        local: 'cq'
    }
}

var bar = Object.assign({}, foo)
bar.name = 'yaya'
bar.info.local = 'bj'

console.log(bar.name)  // yaya
console.log(foo.name)  // muzi

console.log(bar.info.local)  // bj
console.log(foo.info.local)  // bj

深拷贝

var num = 1
var foo = {
    name: 'muzi'
}
var bar = {
    foo: JSON.parse(JSON.stringify(foo))
}
bar.foo.name = 'yaya'
console.log(bar.foo.name)  // yaya
console.log(foo.name)      // muzi

我们将原始对象转换成一个JSON 字符串, 再将该字符串解析成一个结构与原始对象一模一样的对象, 完成了深拷贝

3.3.5 属性描述符

对象中每个属性,都存在属性描述符,可以通过 Object.getOwnPropertyDescriptor() 方法来获取对应的属性描述符。不同的属性描述符代表了不同的作用:

  • value:该属性的值 (仅针对数据属性描述符有效)
  • writable:当且仅当属性的值可以被改变时为 true
  • configurable:当且仅当指定对象的属性描述可以被改变或者属性可被删除时,为 true
  • enumerable:当且仅当指定对象的属性可以被枚举出时,为 true

3.3.6 不变性

作者一共列举出了四个方法:

  • 对象常量:结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性
  • 禁止扩展:Object.preventExtensions(..)
  • 密封:Object.seal
  • 冻结:Object.freeze()

3.3.7 [[Get]]

var myObject = { 
   a: 2
};
myObject.a; // 2

myObject.b; // undefined

myObject.a是怎么取到值2的?
首先它会在对象中查找是否有名称相同的属性, 如果找到就会返回这个属性的值。如果没有找到名称相同的属性,按照 [[Get]] 算法的定义会遍历在原型链上寻找该属性。如果仍然都没有找到名称相同的属性,那 [[Get]] 操作会返回值 undefined(由于仅根据返回值无法判断出到底变量的值为 undefined 还是变量不存在)。

3.3.8 [[Put]]

[[Put]] 被触发时的操作分为两个情况:1. 对象中已经存在这个属性 2. 对象中不存在这个属性.

如果对象中已经存在这个属性,[[Put]] 算法大致会检查下面这些内容:

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

3.3.9 Getter和Setter

对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。
在ES5中可以使用 getter 和 setter 改写部分默认操作,只能应用在单个属性上,无法应用在整个对象上。

getter:是一个隐藏函数,会在获取属性值时调用。同时会覆盖该单个属性默认的 [[Get]]操作。当你设置getter时,不能同时再设置value或writable,否则就会产生一个异常,同时JavaScript 会忽略它的 value 和 writable 特性
使用方式:

var myObject = {
  a: 1111, //会发现myObject.a为2,这是因为设置了getter所以忽略了value特性.
  //方式一:在新对象初始化时定义一个getter
  get a() {
    return 2
  }
};

Object.defineProperty( 
  myObject, // 目标对象 
  "b", // 属性名
  {
    // 方式二:使用defineProperty在现有对象上定义 getter
    get: function(){ return this.a * 2 },
    // 确保 b 会出现在对象的属性列表中
    enumerable: true
   }
);
myObject.a = 3;  //因为设置了getter所以忽略了writable特性.所以这里赋值没成功
myObject.a; // 2
myObject.b; // 4
delete myObject.a;//可以使用delete操作符删除

setter: 是一个隐藏函数,会在获取属性值时调用,同时会覆盖该单个属性默认的 [[Put]]操作(也就是赋值操作)。当你设置setter时,不能同时再设置value或writable,否则就会产生一个异常,同时JavaScript 会忽略它的 value 和 writable 特性
使用方式:

var myObject = {
  //注意:通常来说 getter 和 setter 是成对出现的(只定义一个的话 通常会产生意料之外的行为):
  //方式一:在新对象初始化时定义一个setter
  set a(val) {
    this._a_ = val * 2
  },
  get a() {
    return this._a_ 
  }
};

Object.defineProperty( 
  myObject, // 目标对象 
  "b", // 属性名
  {
    set: function(val){ this._b_ = val * 3 },
    // 方式二:使用defineProperty在现有对象上定义 setter
    get: function(){ return this._b_ },
    // 确保 b 会出现在对象的属性列表中
    enumerable: true
   }
);

myObject.a = 2;  
myObject.b = 3;  
console.log(myObject.a); //4
console.log(myObject.b);//9

console.log(myObject._a_);//4
console.log(myObject._b_);//9

delete myObject.a;//可以使用delete操作符删除

3.3.10 存在性

属性存在性:
如何判断一个对象是否存在某个属性(检查这个属性名是否存在),这时就需要用到:

  1. in操作符
    in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中。
  2. hasOwnProperty(..)
    hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。
var myObject = {
a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false

3.4 遍历

for..in 与 for..of

  • for..in 循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)。
  • 实际上for..in遍历的并不是属性值,而是属性名(即键名 key),所以你想获取属性值还是需要手动使用obj[key]来获取.
  • 一般在遍历对象时,推荐使用for..in,在遍历数组时,推荐还是使用for..of
  • for..of 与 for..in最大的不同点是,它循环的是属性值,而不是属性名,不过它只循环数组里存放的值,不会涉及到对象里的key

例子比较

let arr = ['shotCat',111,{a:'1',b:'2'}]
arr.say="IG niu pi!"
//使用for..in循环
for(let index in arr){
    console.log(arr[index]);//shotCat  111  {a:'1',b:'2'}  IG niu pi!
}
//使用for..of循环
for(var value of arr){
    console.log(value);//shotCat  111  {a:'1',b:'2'}
}
//for..of并没有遍历得到` IG niu pi!`.原因是前面提到的`它只循环数组里存放的值,不会涉及到对象里的key

如何让对象也能使用for..of ?
可以使用上节讲到的Object.keys()搭配使用,例如以下

var shotCat={
    name:'shotCat',
    age:'forever18',
    info:{
    sex:'true man',
    city:'wuhan',
    girlFriend:'小红!'
    }
}
for(var key of Object.keys(shotCat)){
    //使用Object.keys()方法获取对象key的数组
    console.log(key+": "+shotCat[key]);
}

补充: