你不知道的JavaScript(上)笔记2

161 阅读12分钟

1. 为什么要用this

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

随着你的使用模式越来越复杂,显式传递上下文对象会让你的代码变得越来越混乱。

this的误解

指向自身

function foo(num){
    console.log('foo'+ num)
    this.count++
}
foo.count = 0
for(let i=0;i<10;i++){
    if(i>5){
        foo(i)
    }
}
console.log(foo.count) // 0

这里为什么是0而不是4呢

因为for循环中调用的foo函数 是在全局作用域中的,所以foo函数中的this指向的是全局作用域,但是全局作用域中没有count这个属性,就会使用左查询,创建了一个全局变量count且值为NaN

那么如何对上面的代码改动呢?

  1. function foo(num){
        console.log('foo'+ num)
        foo.count++
    }
    foo.count = 0
    for(let i=0;i<10;i++){
        if(i>5){
            foo(i)
        }
    }
    console.log(foo.count) // 0
    

    直接把原来的this改成foo这个函数自身 这避开了this问题,并不是最好的方法

  2. function foo(num){
        console.log('foo'+ num)
        this.count++
    }
    foo.count = 0
    for(let i=0;i<10;i++){
        if(i>5){
            foo.call(foo,i)
        }
    }
    console.log(foo.count) // 0
    

    通过call函数,将foo函数的this改写为foo函数自身,并且将i传递进去

    它的作用域

this在任何情况下都不指向函数的词法作用域

this到底是什么

this是在运行时进行绑定的,并不是在编写时绑定,他的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有关系,只取决于函数的调用位置。

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

2. 全面解析this

调用位置

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

调用位置有时候不是那么好找,因为可能会隐藏真正的调用位置

所以可以使用console.trace()方法进行查看函数中的调用栈。

绑定规则

共有四种绑定规则且四种规则中具有优先级

默认绑定

函数不使用任何修饰的函数引用进行调用的,只能使用默认绑定。this指向全局对象

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

如果使用严格模式(strict mode)则不能将全局对象用于默认绑定,因此this会绑定到undefined

function foo(){
    'use strict';
    console.log(this.a)
}
var a=2
foo() // TypeError: this is not undefined

如果在函数内部使用严格模式然后调用foo函数则不影响默认绑定

function foo(){
    console.log(this.a)
}
(function(){
    'use strict';
    var a=2
    foo() // 2
})()

避免在代码中严格模式和非严格模式混用

隐式绑定

调用位置是否有上下文,或者说是否被某个对象拥有或者包含。

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

当函数引用有上下文时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为foo()调用时 this绑定到obj所以this.a和obj.a是一样的。

对象属性引用链中只有上一层或者说是最后一层调用位置起作用:

function foo(){
    console.log(this.a)
}
var obj2= {
    a:22,
    foo:foo
}
var obj1={
    a:33,
    obj2:obj2
}
obj1.obj2.foo() // 22
隐式丢失

隐式绑定函数会丢失绑定对象,会使用默认绑定,从而把this绑定到全局作用域或者是undefined

function foo(){
    console.log(this.a)
}
var obj={
    a:2,
    foo:foo
}
var bar = obj.foo // 函数别名
var a = 'hello'
bar() // 'hello'

还有以下一种情况 ,在回调函数中隐式绑定丢失

function foo(){
    console.log(this.a)
}
function doFoo(fn){
    // fn 引用的其实是foo
    fn() //调用位置
}
var obj={
    a:2,
    foo:foo
}
var a = 'hello'
doFoo(obj.foo) // hello 

我个人的理解就是在哪里调用的this就是指向谁,上面这个回调函数中fn调用于doFoo函数中 然后doFoo调用于全局作用域中 那么this就是全局作用域。

显示绑定

显示绑定 就是使用call、apply将某个对象绑定到函数的this上。

硬绑定
function foo(){
   console.log(this.a)
}
var obj={
    a:2,
}
function bar(){
    foo.call(obj)
}
bar() // 2
setTimeout(bar(),1000) // 2

使用一个函数进行封装他 然后返回执行结果

可以封装成以下函数

function foo(something){
    console.log(this.a+something)
    return this.a +something
}
// 简单的辅助绑定函数
function bind(fn,obj){
    return function (){
        return fn.call(obj,arguments)
    }
}
var obj ={
    a:2
}
var bar = bind(foo,obj)
var b=bar(4) //2 4
console.log(b) // 6

这样写可以避免绑定丢失的问题

其实bind函数就是上面的简洁写法。bind函数绑定后返回一个函数再调用。

调用上下文API

很多第三档函数提供了可以更改this指向的参数。

例如:

function foo(el){
    console.log(el,this.id)
}
var obj={
    id:'123'
}
[1,2,3].forEach(foo,obj) // 1 123 2 123 3 123

new绑定

误解:

JavaScript中的new操作符调用的函数叫做构造函数 。这个函数就是一个普通的函数 ,只是因为被new标识符调用。

实际上不存在所谓的“构造函数”只有对于函数的“调用构造”

调用构造函数时会发生一下操作:

  1. 创建一个全新的对象
  2. 这个对象会被执行[[Prototype]]连接
  3. 这个新对象会绑定调用函数的this
  4. 如果函数没有返回其他值,那么就会返回这个新对象。

优先级

当这些规则交叉使用时,哪一条规则的优先级更高呢。

默认绑定的优先级是最低的,那么隐式绑定和显示绑定谁的优先级更高呢?

function foo(){
    console.log(this.a)
}
var obj1={
    a:2,
    foo:foo
}
var obj2 = {
    a:3,
    foo:foo
}
obj1.foo() // 2
obj2.foo() // 3
obj1.foo.call(obj2) // 3
obj2.foo.call(obj1) // 2
显示绑定的优先级比隐式绑定高

new绑定和隐式绑定谁的优先级更高?

function foo(num){
    this.a=num
}
var obj1 ={
    foo:foo
}
var obj2={}
obj1.foo(2) 
console.log(obj1.a) // 2
​
obj1.foo.call(obj2,3)
console.log(obj2.a) // 3var bar = new obj1.foo(4)
console.log(obj1.a) // 2
console.log(bar.a) // 4
结论:new绑定的优先级高于隐式绑定

new绑定不能直接和显示绑定进行比较 需要中转一下,就是用new绑定和硬绑定进行比较。

function foo (num){
    this.a = num
}
var obj1={}
var bar = foo.call(obj1)
bar(2)
console.log(obj1.a) // 2
var baz = new bar(3)
console.log(obj1.a) // 2
console.log(baz.a) // 3
由此可见new绑定的优先级高于显示绑定 

绑定例外

被忽略的this

如果你将null、undefined作为参数传递进call、apply、bind函数,这些值在调用时会被忽略,会使用默认绑定。

Object.create(null) 使用这种创建空对象 不会继承Object的原型对象,比{}字面量更空。

其实你并不关心你传递的this,但是如果传递null或者undefined的话会导致一些比较隐蔽的问题。所以建议自己创建一个空的对象 ,然后以这个对象当做this传递进去。因为你知道这个对象没有使用过,所以不会起冲突。

function foo(a,b){
    console.log('a:'+a+',b:'+b)
}
var DMZ = Object.create(null)
foo.apply(DMZ,[2,3]) // a:2 b:3
var bar = new foo.bind(DMZ,2) // bind参数可以向下传递
bar(3) // a:2 b:3

this语法

箭头函数本身没有this 是继承自上一层的this

3. 对象

3.3.5 属性描述符

从ES5开始后所有属性都拥有了属性描述符

var obj = {
    a:2
}
Object.getOwnPropertyDescriptor(obj,'a');
/**
{
    value:2,
    writable:true, // 可写性
    enumerable:true, // 可枚举性
    configurable:true, // 可配置性 单向配置 不可逆!!(改了一次之后就不能再改了)
}
*/

方法

1. Object.getOwnPropertyDescriptor(obj,‘属性名’)   
​
   查看这个对象的属性标识符
​
2. Object.defineProperty(obj,'属性名',{
​
    value:'2',
    writable:true,
    configurable:true,
    enumerable:true
   })
    修改这个属性的属性标识符
    注:configurable:false后还能改变writable:false 但是不可逆 而且delete 属性名 失败
   
3. Object.hasOwnProperty('属性名') // 检查对象中是否拥有这个属性

3.3.6 不可变性

es5中希望对象不可变,可以通过以下方法实现

所有的方法创建的都是浅不变性 不变性只能影响他的直接属性 如果目标对象引用了其他对象(函数、数组等),其他对象仍是可变的。

对象常量

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

禁止扩展

如果你想禁止一个对象新增属性但是保留原本的属性可以使用

var obj = {
    name:'gsy'
}
Object.preventExtensions(obj)
obj.age=14
console.log(obj.age) // undefined

密封

Object.seal()会创建一个密封的对象,这个方法会在现有的对象上调用Object.preventExtensions()方法并且把所有的属性标记为configurable:false

密封后的对象不能添加新的属性也不能重新配置或者删除属性。

冻结

Object.freeze(...)会创建一个冻结对象。这个方法实际上是调用了Object.seal(...)并且把所有属性标记为writable:false

这样就无法修改他的值

3.3.7 [[Get]]

var obj = {
    a:'xxx'
}
console.log(obj.a) // xxx
console.log(obj.b) // undefined

首先会在当前对象中查询 如果没有就会去原型链中查询 如果都没有找到这个属性就会返回undefined

注意: 这种方法和访问变量时是不一样的。如果你引用一个当前词法作用域中不存在的变量不是想对象属性那样返回undefined 而是返回ReferenceError。

3.3.8 [[Put]]

[[Put]]触发时,实际取决于许多因素,(主要时这个对象中是否包含了这个属性)

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

3.3.9 [[Get]]和[[Setter]]

当你给属性定义了setter和getter后,这个属性会被定义为“访问描述符”。对于访问描述符来说,会忽略属性的value和writable特性。只关心get和set。

var Obj = {
    get a(){
        return 2;
    }
}
​
Object.defineProperty(obj,'b',{
    get:function(){
        return this.a*2
    } 
})
obj.a // 2
obj.b // 4

如果只定义get而不定义set就会导致你在给对象属性赋值时失败。通常getter个setter是一对出现的,避免单个出现(可能会出现问题)

3.3.10 存在性

为了区分出这个对象的属性的值就是undefined还是因为没有定义而是undefined。可以使用以下方法进行判断

var obj = {
    a:2
}
('a' in obj) // true
('b' in obj) // false
​
obj.hasOwnProperty('a') // true
obj.hasOwnProperty('b') // false

in操作符会检查属性是否在对象及其[[Propertype]]原型链上

hasOwnProperty只会检查属性是否在对象中

Tips: in操作符可以检查容器内是否有某个值,但是它实际上是检查某某属性名存不存在,对于数组来说这个区别很重要,4 in [2,4,6]的结果并不是true,因为[2,4,6]这个数组中包含的属性名是0、 1 、 2没有4

枚举

设置属性标识符的enumerable:false后,将无法使用for in 对 对象进行遍历到该属性。

原型对象

此处只写一些有意思的

在类继承中,想让子类继承父类的原型对象 有以下几种方式:

一种是通过实例化new 来继承 一种是通过Object.create(...)来实现 当你改变了原型对象默认的值都会丢失原型对象指向的构造函数 此时需要你手动重新赋值 (如果你需要的话)

function Father(){
    this.name='dad'
}
Father.prototype.eat=function(){
    console.log('吃饭')
}
​
function Son(){
    this.name='son'
}
// 1. Son.prototype = Father.prototype // 弊端:修改子类的原型对象函数 父类也会跟着变化 因为是赋值的一个引用类型。pass// 2. Son.prototype = new Father() // 这样写也不行 因为实例化Father(...) 这个构造函数中可能会有副作用 比如给this添加了新的属性了 会影响到后续继承的子类中。所以才会有第三种的组合继承用个空的构造函数来解决这个问题。// 3. 组合继承
function link(){}
Link.prototype = Father.prototype // 将原型对象赋值给中间件原型对象 再进行实例化就切断了与父类构造函数的联系
Son.prototype = new Link() // 通过实例化一个无副作用的空构造函数作为中介
Son.protoype.constructor=son // 通过改变原型对象后 构造函数会丢失 需要重新赋值
let father = new Father()
let son =  new Son()
son.eat = function(){
    console.log('吃面条')
}
son.eat() // 吃面条
father.eat() // 吃饭//4. 还有一种办法 Object.create(...)
// Object.create(...) 会创建一个对象然后将原型对象关联到传入的对象上
Son.prototype = Object.create(Father.prototype) // 如果你需要原型对象的构造函数指向 需要重新给他赋值就和上面一样
let father = new Father()
let son =  new Son()
son.eat = function(){
    console.log('吃面条')
}
son.eat() // 吃面条
father.eat() // 吃饭// 4. Object.setPrototypeOf(obj.prototype,obj1.prototype)
// 设置原型对象在es6之后Object.setPrototypeOf(Son.prototype,Father.prototype)
let son = new Son()
let father = new Father()
son.prototype.eat=function(){
    console.log('吃汉堡')
}
son.eat() // 吃汉堡
father.eat() // 吃饭

类 (ES5)

javascript 中没有类 只是模仿类。这里指的是和java和php等语言对比。

他的类就是对象之间的混入,继承就是对象混入的思想。

对象和对象之间的联系就是通过原型对线来链接的

构造函数 指的是构造函数调用 只是被new 操作符调用的函数都叫构造函数调用 其实就是个函数

使用类的编程思想和委托模式 相对来说委托更加简单明了 向一个对象的原型对象中插入一些方法和属性 实现委托。比类继承结构更清晰

Object.create(...)真的非常好用,创建一个空对象然后把传入的对象作为原型对象指向空对象。少去了很多副作用 (ES5之后)

在ES5之前的话需要自己手写个 其实就是创建一个空的函数切断副作用罢了

if(!Object.create){
    Object.create=function(o){
       function Foo(){}
       Foo.prototype = 0
       return new Foo()
    }
}

使用类来实现:

function Father(name){
    this.name = name
}
Father.prototype.say=function(){
    console.log(this.name)
}
​
function Son(name){
    Father.call(this,name)
    this.age = 18
}
​
Son.prototype = Object.create(Father.prototype)
​
var a = new Father('xm')
var b = new Son('xg')
a.say() // xm
b.say() // xg

使用委托的设计模式实现

var a = {
    init:function(name){
        this.name = name
    },
    say:function(){
        console.log(this.name)
    }
}
​
var b = Object.create(a)
​
b.age = 18var c = Object.create(b)
var d = Object.create(b)
c.init('xg')
d.init('xm')
c.say() // xg
d.say() // xm

不需要使用构造函数 和类就能实现 功能相同