《你不知道的Javascript--上卷 学习总结》(this和对象)

171 阅读7分钟

this全面解析

调用栈

函数调用位置就是函数在代码中被调用的位置(而不是声明的位置),寻找调用位置最重要的就是分析调用栈

this绑定规则

  • 默认绑定(独立函数调用 fn)

这个时候的this是window(注意:如果使用严格模式,则不能将全局对象用于默认绑定

虽然this的绑定规则完全取决于调用位置,但是只有函数运行在非strict mode下时,默认绑定才能绑定到全局对象;在严格模式下调用函数则不影响默认绑定。

    function foo(){
        'use strict'
        console.log(this.a)
    }
    
    var a = 2;
    
    foo(); // TypeError : this is undefined
    
    
    function foo(){
        console.log(this.a)
    }
    
    var a = 2;
    
    (function(){
        'use strict'
        
        foo() // 2
    })()
  • 隐式绑定(obj.fn)

调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,这时候隐式绑定规则会把函数调用中的this绑定到这个上下文对象。

    function foo(){
        console.log(this.a)
    }
    
    var obj = {
        a:2,
        foo:foo
    }
    
    obj.foo() // 2

隐式规则会有时候丢失绑定对象。

1、定义函数别名导致丢失

    function foo(){
        console.log(this.a)
    }
    
    var obj = {
        a:2,
        foo:foo
    }
    
    var bar = obj.foo;
    
    var a = 'xx'
    
    bar() // 'xx'  // 这个时候导致this 指向的全局
    

2、当做回调函数

    function foo(){
        console.log(this.a)
    }
    
    var obj = {
        a:2,
        foo:foo
    }
    
    var a = 'xx'
    
    setTimeout(obj.foo,100) // 'xx'
  • 显示绑定(obj.bind/apply/call,bind实现考虑作为构造函数

传递的第一个参数是this对象,如果传入的是一个原始值来当做this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String()、new Boolean()或者new Number()),就是装箱。

    function foo(){
        console.log(this.a)
    }
    
    var obj = {
        a:2
    }
    foo.call(obj) // 2
    
    
    
    function foo(something){
        console.log(this.a,something);
        return this.a + something;
    }
    
    // 辅助函数bind
    function bind(fn,obj){
        return function(){
            return fn.apply(obj,arguments)
        }
    }
    
    var obj = {
        a:2
    }
    
    var bar = bind(foo,obj)
    
    var b = bar(3) ;
    console.log(b) // 5
  • new绑定(var a = new A())

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

1、创建一个全新的对象。

2、这个新对象会被执行[[prototype]]连接

3、这个新对象会绑定到函数调用的this

4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

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

优先级

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

绑定例外

如果显示绑定的this传递的null或者undefined,实际应用的是默认绑定规则。

创建一个安全的空对象的最简单的方法就是使用Object.create(null),它创建的空对象不会创建Object.prototype这个委托,所以他比{}更空。

赋值表达式p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo() 或者 o.foo() ,因此这里会应用默认绑定。

    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

对于默认绑定来说,决定this绑定对象并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局。

箭头函数的this

箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。

    function foo(){
        return (a) => {
            // this 继承自foo()
            console.log(this)
        }
    }
    
    var obj1 = {
        a:2
    }
    
    var obj2 = {
        a:3
    }
    
    var bar = foo.call(obj1);
    bar.call(obj2) // 2

对象

类型

主要类型

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

内置对象

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

内容

1、在对象中,属性名永远都是字符串,如果你使用string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串,即使是数字也不例外。

2、数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性.但是数组的length值并未发生改变

3、如果你试图向数组添加一个属性,但是属性名是一个数字,那么就会变成数值下标(因此会修改数组的内容而不是添加一个属性)

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

属性描述符(数据描述符)

对象默认的属性描述符

    var myObject = {
        a:2
    }
    Object.getOwnPropertyDescriptor(myObject,a)
    
    /*
        {
            value:2,
            writable:true, // 可写
            configurable:true, // 可配置
            enumerable:true  // 可枚举
        }
    
    */

自己设置属性描述符

     var myObject = {
    }
    
    Object.defineProperty(myObject,a,{
        value:2,
        writable:true, 
        configurable:true, 
        enumerable:true 
    })

writable

决定是否可以修改属性的值,true为可以,false为不可以

configurable

决定属性是否可配置,如果通过defineProperty定义一个属性configurable为false,则不可以在进行配置(defineProperty),configurable:false还会禁止删除这个属性

注意:即使属性是configurable:false ,我们还是可以把writable的状态由true改为false,但是无法由false改为true

enumerable

控制属性是否会出现在对象的属性枚举中,比如说for..in循环。

不变性

1、对象常量

结合writable:false和configurable:false就可以创建一个真正的常量属性(不可修改、重定义或者删除)

2、禁止扩展

如果想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions()

   Object.preventExtensions(myObject) 

3、密封 Object.seal()(Object.preventExtensions加所有属性标记为configurable:false)

4、冻结 Object.freeze() (Object.seal加把所有的"数据访问"属性标记为writable:false)

[[Put]]

[[Put]]被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素),如果已经存在这个属性,[[Put]]算法大致会检查下面这些内容。

1、属性是否是访问描述符?如果是并且存在setter就调用setter。

2、属性的数据描述符中writable是否为false?如果是,在严格模式下静默失败,在严格模式下抛出TypeError异常。

3、如果都不是,将该值设置为属性的值。

Getter和Setter

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

当你给一个属性定义getter、setter或者两者都有时,这个属性会被定义为"访问描述符"(和"数据描述符"相对)。对于访问描述符来说,Javascript会忽略它们的value和writable特性,取而代之的是关心set和get特性。

    var myObject = {
        // 给a定义一个getter
        get a() {
            return 2
        }
    }
    
    Object.defineProperty(
        myObject,
        "b",
        {
            get:function(){
                return this.a *2
            },
            enumerable:true
        }
    )
    myObject.a = 3  // 没有设置成功 因为没有set,所以一般get和set要成对出现
    myObject.a // 2
    myObject.b // 4
    

存在性

in操作符会检查属性是否在对象及其[[Prototype]] 原型链中。(实际上检查的是某个属性名是否存在)

hasOwnProperty只会检查属性是否在myObject对象中,不会检查[[Prototype]]链。

enumerable:true 就相当于"可以出现在对象属性的遍历中"。

propertyIsEnumerable会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true。

Object.keys会返回一个数组,包含所有可枚举属性。

Object.getOwnPropertyNames会返回一个数组,包含所有属性,无论他们是否可枚举。

in和hasOwnProperty的区别在于是否查找[[Prototype]]链。

object.keys和object.getOwnPropertyNames都只会查找对象直接包含的属性。

遍历

如果想直接遍历数组的值可以使用for..of循环语法。(如果对象本身定义了迭代器的话也可以遍历对象)

for..of循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。

    var myArray = [1,2,3]
    for(var v of myArray){
        console.log(v) // 1,2,3
    }

数组有内置的@@iterator,因此for..of可以直接应用在数组上。我们使用内置的@@iterator来手动遍历数组,

    var myArray = [1,2,3];
    var it = myArray[Symbol.iterator]();
    
    it.next() // {value:1,done:false}
    it.next() // {value:2,done:false}
    it.next() // {value:3,done:false}  
    it.next() // {done:true}   value为当前的遍历值   done是一个布尔值,表示是否还有可以遍历的值

普通的对象如何实现for..of遍历(需要自己定义Symbol.iterator)

    var myObject = {
        a:2,
        b:3
    }
    
    Object.defineProperty(myObject,Symbol.iterator,{
        enumerable:false,
        writable:false,
        configurable:true,
        value:function(){
            var o = this;
            var idx = 0;
            var ks = Object.keys(o);
            return {
                next:function(){
                    return {
                        value:o[ks[idx++]],
                        done:(idx > ks.length)
                    }
                }
            }
        }
    })
    
    // 手动遍历myObject
    var it = myObject[Symbol.iterator]();
    it.next(); // {value:2,done:false}
    it.next();// {value:3,done:false}
    it.next();// {value:undefined,done:true}
    
    // 用for..of遍历myObject
    for(var v of myObject){
        console.log(v)
    }