this

js代码编译阶段,会创建执行上下文,又会创建当前上下文的词法环境和变量环境,this就是词法环境记录的一个属性。
this是在运行时(函数被调用时)绑定的,不是在编译/编写时候。this和函数声明位置没有关系,只和函数调用位置有关系,它指向哪里完全取决函数在哪里被调用。这点和词法作用域相反,词法作用域和函数调用方式无关,和声明位置有关。

调用方式和执行上下文栈

前面说this只和函数调用方式有关系,什么是调用方式?就是函数在代码中调用的方式(听君一席话,全部是废话)调用方式还要从执行上下文和执行上下文栈说起。当我们调用任何函数时候,这个函数引用有执行上下文对象,this就会绑定在这个执行上下文,多层引用只有最后一层有效。

4种this绑定方式
默认绑定

以下代码中foo调用,是不带任何修饰符的调用,this默认绑定,指向全局。

1
2
3
4
5
6
function foo() {
console.log(this.a) // this指向window
}
var a = 2;
// 其实这里也相当于window.foo()
foo(); // 2
隐式绑定(上下文调用)

当调用位置有上下文对象时候,比如以下f函数调用方式,是通过obj的上下文来引用的,隐式绑定会把函数中的this绑定到这个上下文对象。函数被调用时,obj对象拥有它,因此this指向obj

1
2
3
4
5
6
7
let obj = {
f(){
console.log(this)
}
}

obj.f() //obj引用了这个函数,this指向obj

当具有多层引用时候,只有最后一层有效

1
2
3
4
5
6
7
8
9
let obj = {
f(){
console.log(this)
}
}
let obj1 = {
obj:obj
}
obj1.obj.f() //多层引用,最后一层有效,this指向obj

隐式绑定丢失的情况,bar函数引用的是f函数本身,所以调用时bar()时候相当于不带任何修饰,会应用默认绑定

1
2
3
4
5
6
7
let obj = {
f(){
console.log(this) // window
}
}
var bar = obj.f //把f函数本身给了bar
bar() // 相当于window.bar()

函数作为参数,也会发生隐式绑定丢失,因为参数传递也是一种赋值

1
2
3
4
5
6
7
8
9
10
let obj = {
f(){
console.log(this) // window
}
}
function doFoo (fn) {
// 这里相当于 var fn = fn(),所以相当于window.fn()
fn()
}
doFoo(obj.f)

setTimeout,setInterval等内置js函数中,也会发生隐式绑定丢失

1
2
3
4
5
6
let obj = {
f(){
console.log(this) // window
}
}
setTimeout(obj.f,100)
显式绑定(call,apply,bind)

如果我们想将函数调用强制指向某个对象,应该怎么做?可以采用callapply,这两个api的作用相同,唯一的不同就是传参方式不同,两者性能差异基本可以忽略,如果非要说性能的话,call会比apply好一点,ECMA官网的介绍中,apply会比call算法上多一个CreateListFromArrayLike的步骤。除了这两种以外,还有一个ES5的内置函数,bind方法

  • call(Obj,params1,params2,...) 会立即调用并且执行
  • apply(Obj,[params1,params2,...]) 会立即调用并且执行
  • Function.prototype.bind(Obj) 不会立即调用,返回新函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let obj = {
    f(){
    console.log(this) // obj
    }
    }
    var bar = obj.f //把f函数本身给了bar
    bar.call(obj) // 通过call修改this指向,所以this是obj,会立即执行
    bar.apply(obj) // apply调用也一样,会立即执行

    var newBar = bar.bind(obj) // 修改this之后会返回一个新的函数,不会立即执行
    newBar()
    除此之外,js和宿主环境的内置函数,都有一个可选参数,来确保回调函数使用指定的this,比较常见的就是forEachmap。这两个函数最后都有一个参数,用来确保当前回调函数使用指定的this
  • Array.prototype.forEach(fn,[thisArg])
  • Array.prototype.map(fn,[thisArg])
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function foo(el) {
    console.log(el,this.id)
    };

    var obj = {
    id:"awesome"
    };

    [1,2,3].forEach(foo,obj) // awesome 会输出三次,因为将this显式绑定在了obj
    [1,2,3].map(foo,obj) // map也相同
    这里提到了循环的forEachmap,顺带说一下循环效率问题,在我之前测试中,for循环速度最快,其次是forEach,最后才是map
    new绑定
    在介绍new绑定之前,先说一下new操作符。
  • js中,构造函数只是使用new操作符被调用的函数。它们不属于某个类,也不会实例化一个类。
  • 构造函数也不是特殊的函数类型,只是被new操作符调用的普通函数。
  • 不存在构造函数,只有构造调用

以上介绍来自你不知道的javascript。说白了,在js中,不存在构造函数,只有new操作符调用的普通函数,也就是构造调用。

那么,当我们使用new操作符来调用函数时候,或者说发生构造调用,和我们普通调用函数有什么区别呢?

1.创造一个全新的对象

2.这个新的对象会执行[[Prototype]]链接

3.新对象会绑定到函数调用的this

4.如果这个函数没有其他返回对象(意思是用户的函数返回的不是一个对象,而是一个普通的值,那么就丢弃掉),那么new表达式中的函数调用会返回这个新的对象

实际上,在ECMAScript规范中,需要的步骤会更多,以上只是简化版本。简单来讲,通过new调用,会创建一个新的对象,并且将用户的函数this绑定到这个对象上,执行函数返回。

当我们知道了以上这几点时候,我们可以来尝试一下自己实现一个new函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function newn() {
// 1.创建一个新对象
let obj = new Object()
// 2.获取到用户传入的函数以及参数,这里可以通过arguments对象获取,arguments参数是类数组
// arguments可以通过[].slice.call(arguments),Array.from()等方法转换为数组
let [fn, ...args] = Array.from(arguments)
// 3.修改prototype
obj.__proto__ = fn.prototype
// 4.执行函数,并且将this绑定到新创建的对象,这里的result就是用户传入的函数执行后的返回值
let result = fn.call(obj, ...args)
// 5.如果用户函数本来有返回值,并且是一个对象就返回这个对象,否则返回新创建的对象
return result instanceof Object ? result : obj
}
function bar(params) {
this.a = params
}
let foo = newn(bar, 10)
console.log(foo)
例外情况:箭头函数绑定

箭头函数是es6中的语法

  • 箭头函数没有自己的this,arguments,super
  • 箭头函数没有prototype
  • 箭头函数不可以使用new调用
  • 箭头函数的this绑定无法被修改

什么叫做没有自己的this,arguments,super呢?也就是箭头函数中的this并不是自己的this,而是根据外层作用域来决定的,也可以说是根据当前的词法作用域来决定的。来看以下的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
return a => {
// 没有自己的arguments,因为使用的外层作用域的!
console.log(arguments);
// this继承自foo
console.log(this.a);
};
}
var obj1 = {
a: 2
};

var obj2 = {
a: 3
};
// 执行foo,将this指向obj1,此时bar函数就是箭头函数
var bar = foo.call(obj1);
// 执行返回的箭头函数,并且再次尝试绑定this,但是绑定失败!因为箭头函数在执行foo函数,已经被绑定了!
bar.call(obj2); // 2,不是3!

// 这个例子来自你不知道的javascript

其实以上代码相当于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
// 保存this
var self = this;
return function(a) {
// 使用外层this
console.log(self.a);
};
}
var obj1 = {
a: 2
};

var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2

总结this绑定,一共有4种绑定

  • 默认调用。绑定到全局对象
  • 隐式调用(上下文调用),绑定到那个上下文对象
  • 显式调用(call,apply,bind),绑定到指定对象
  • new调用,绑定到新创建的对象
  • 例外情况:箭头函数,词法作用域决定this,绑定到外层作用域

原型

__proto__或者叫([[Prototype]])

js中所有的对象(函数也是对象)都有一个__proto__内置属性,是一个对象,就是对于其他对象的引用,是隐式原型。和[[Prototype]]意义相同,注意是[[Prototype]]不是prototype

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
a:1
}
// 访问属性时候先访问[[Get]](),有相同的名字返回,没有就查找__proto__,还没有就会返回undefined。最后面对象一节有介绍
obj.a // 1

var obj = {
a:1
}
var myObj = Object.create(obj) // 将myobj的__proto__指向obj
myObj.a // 1

原型链

简单一点说,就是在访问对象属性,实际上就是在执行[[Get]](),按照查找规则就是

  • 查找自身,自身没有查找原型
  • 原型对象也是对象,也会被执行[[Get]]()操作
  • 最终尽头是Object.prototype.__proto__,值为null

这样不断查找关联起来的就叫做原型链,Object.prototype有很多通用方法都在上面,比如toString(),valueOf()

prototype

prototype是每个函数都有的公开属性,且不可枚举,指向另外一个对象,是显式原型

1
2
3
4
function foo () {

}
foo.prototype // {constructor:f()}
constructor

constructorprototype公有并且不可枚举的属性,这个属性引用是这个函数的本身

1
2
3
4
5
6
function foo () {

}
var bar = new foo()
foo.prototype.constructor === foo // true
bar.constructor === foo // true
prototype和__proto__、constructor之间的关系
1
2
3
4
5
6
7
8
9
10
function Foo () {

}
var foo = new Foo()
var foo1 = new Foo()
foo.__proto__ === Foo.prototype // true
foo.__proto__ === foo1.__proto__ // true

Foo.prototype.constructor === Foo // true
foo.constructor === Foo // true
  • 1.为什么以上代码的foo.__proto__Foo.prototype全等?回想之前的this绑定中的new绑定执行的操作,在第三步时候,将新创建的对象的__proto__指向函数的prototype,最后返回这个对象。所以foo.__proto__ === Foo.prototype
    1
    2
    3
    4
    5
    6
    7
    8
    function newn() {
    ...
    // 3.修改prototype
    obj.__proto__ = fn.prototype
    ...
    // 5.如果用户函数本来有返回值,并且是一个对象就返回这个对象,否则返回新创建的对象
    return result instanceof Object ? result : obj
    }
  • 2.为什么foo.constructor === Foo?同样,也是因为在new的时候,执行了第三步,也就是
    1
    2
    3
    4
    5
    6
    obj.__proto__ = fn.prototype
    // fn.prototype就是
    fn.prototype = {
    constructor:fn()
    }
    最后赋值给obj.__proto__
    所以,当查找foo.constructor,并不是自身的constructor,根据对象查找规则,自身没有就会查找__proto__,这里就是查找的__proto__上的constructor
    最后画一张图
    image
    函数和对象的关系
  • 1.js内置对象本质上是函数,通过new调用,就是构造函数。所有函数都继承于Function
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    - String
    - Number
    - Boolean
    - Object
    - Function
    - Array
    - Date
    - RegExp
    - Error

    对应就是
    ƒ String() { [native code] }
    ƒ Number() { [native code] }
    ƒ Boolean() { [native code] }
    ƒ Object() { [native code] }
    ƒ Function() { [native code] }
    ƒ Array() { [native code] }
    ƒ Date() { [native code] }
    ƒ RegExp() { [native code] }
    ƒ Error() { [native code] }
    其实可以看到,本质上都是函数,只要是函数,都继承于Function,包括Function本身和Object
    1
    2
    3
    Function.__proto__ === Function.prototype // true
    Number.__proto__ === Function.prototype // true
    Object.__proto__ === Function.prototype // true
  • 2.所有的对象都继承于Object,函数也是对象,所有对象原型链终点是Object.prototype
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var str = new String('i am string')
    str instanceof Object // true
    str instanceof String // true
    // 原型链表示为
    str → String.prototype → Object.prototype → null

    function f(){}
    f.__proto__ === Function.prototype // true
    // 原型链表示为
    f → === Function.prototype → Object.prototype → null
    1
    2
    3
    4
    5
    6
    7
    为什么?因为Object函数继承于Function,所以Object.__proto__ === Function.prototype

    Object instanceof Function // true

    为什么?因为Function也继承于Function,所以Function.__proto__ === Function.prototype,Function.prototype的__proto__指向Object.prototype

    Function instanceof Object // true
    所以对象都包含Object原型上的方法,Number,String,Booelan是包装对象,也是对象
    image
    原型小结
  • 每个对象都有一个原型对象__proto__,访问对象属性会触发内部的[[Get]]()函数,先查找自身,在查找__proto__中指向的对象,指向的对象也是对象,这样不断查找,构成了原型链
  • __proto__是每个对象都有的属性,隐式原型
  • prototype是每个函数都有的属性,显式原型
  • constructorprototype的一个属性,指向其函数自身

继承

原型链继承

这种继承方式有两个最大的问题

  • 1.多个子类实例化时候,修改了父类,父类属性会被共享
  • 2.无法在不影响父类情况下,给函数传递参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    function Super () {
    this.property = true
    this.arr = []
    }
    Super.prototype.getSuperValue = function () {
    return this.property
    }
    Super.prototype.setPush = function () {
    this.arr.push(1)
    }
    Super.prototype.getArr = function () {
    return this.arr
    }
    function Sub () {
    this.subProperty = false
    }
    // 相当于Sub.prototype = Super.prototype
    // 原型继承的关键
    Sub.prototype = new Super()
    Sub.prototype.getSubValue = function () {
    return this.subproperty;
    };
    // 所以此时,Sub会拥有其父类的方法
    let instance = new Sub()
    let instance1 = new Sub()
    // 这里调用的就是.__proto__上面父类的方法
    console.log(instance.getSuperValue()) // true
    // 修改其中一个子类
    console.log(instance.setPush())
    // 只要一个子类修改了父级属性,所有子类都会被共享
    console.log(instance1.getArr()) // [1]
    问:如果我直接通过这种写法Sub.prototype = Super.prototype,直接将原型关联起来,可不可以?

当然是不可以,这样的话就是直接引用了父类的prototype,修改子类会导致父类的prototype也发生变化,通过new关键字,就可以创建新的对象,将.__proto__指向父类的prototype

借用构造函数
  • 这种继承方式,虽然解决了原型链继承不能传递参数问题,但是方法要在构造函数中定义。
  • 还有子类无法访问父类的方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 借用构造函数
    function Super (name) {
    this.name = name
    }
    function Sub () {
    // 这里通过call执行了父类方法,this指向子类,相当于子类也有name属性
    // 继承并且传参
    Super.call(this,"John")
    this.age = 20
    }
    const instance = new Sub()
    // 此时instance会有age和name两个属性
    console.log(instance) // {name: "John", age: 20}
    组合继承
    组合继承是原型链继承和构造函数继承两种方法的结合。
  • 原型链继承方法
  • 借用构造函数继承属性
    这种方式弥补了原型链继承和构造函数继承的缺点可以创建多个互不影响的实例,子类有自己的属性和方法,也可以共享父类属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function Super(name) {
    // 定义父类实例属性
    this.name = name
    this.age = 18
    }
    // 定义父类实例方法
    Super.prototype.sayName = function () {
    console.log(this.name)
    }

    function Sub(name) {
    // 借用构造函数继承父类实例属性
    // 父类第二次被调用
    Super.call(this, name)
    }
    // 原型链继承父类实例方法
    // 父类第一次被调用
    Sub.prototype = new Super()
    const instance = new Sub('John')
    console.log(instance, 'instance') // {name: "John", age: 18}
    const instance2 = new Sub('Bob')
    console.log(instance2, 'instance') // {name: "Bob", age: 18}

    不过这种方法也有问题,就是父类构造函数始终会被调用两次
    原型式继承
    其实就是Object.create()方法的polyfill实现方法。ES5引入了Object.create()

根据红宝书第四版介绍,使用场景为一般在你想基于一个对象创建另外一个对象时候,可以使用原型式继承也就是Object.create()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function object(o) {
function F() {}
F.prototype = o
// {}.__proto__ = F.prototype = 传入的对象
return new F()
}
let instance = object({ a: 1 })
instance.b = 10
console.log(instance) // {b:10,__proto__:{a:1}}
console.log(instance.b) // 10
console.log(instance.a) // 1

// 同Object.create效果一样
let instance = Object.create({ a: 1 })

其实看上去可能有点迷惑,不过我们只需要知道,这样做的最终目的无非两点

  • 1.返回新对象实例
  • 2.新对象实例的.__proto__指向我们传入的对象,这样就可以调用和共享我们之前的方法和属性,也可以拓展我们现有对象自己的属性和方法
寄生式继承

如果我们只想关注对象,不关注方法,还可以通过寄生式继承来实现,寄生式继承基于原型式继承实现

1
2
3
4
5
6
7
8
9
10
11
12
13
function getName(rawObj) {
// 原型式继承
const o = Object.create(rawObj)
// 对象上定义方法
o.sayName = function () {
return 'sayName'
}
return o
}
const instance = getName({ name: 'John' })
// 直接调用定义的方法
console.log(instance.sayName()) // sayName

寄生组合式继承

这种方式可以解决组合式继承多次调用父类函数的问题

1
2
3
4
5
6
7
8
function inheritPrototype(Sub, Super) {
// 通过原型式继承创建父类副本
// prototype是一个对象,其__proto__指向Super.prototype
const prototype = Object.create(Super)
// 修复constructor,constructor是prototype的一个属性,指向构造函数本身,这样写会被覆盖,需要重新设置
prototype.constructor = Sub
Sub.prototype = prototype
}

垃圾回收

参考写的另外的一篇垃圾回收文章
V8中的垃圾回收器与内存管理

参考资料:

https://juejin.cn/post/6844903953981767688 JavaScript:V8编译过程

https://juejin.cn/post/6961349015346610184 Event Loop 和 JS
引擎、渲染引擎的关系
https://www.cnblogs.com/wangfupeng1988/p/3977924.html 深入理解javascript原型和闭包(完结)

https://blog.csdn.net/szengtal/article/details/78726143 词法环境:ECMA实现(一)— 简介

https://juejin.cn/post/6926831181681917959#heading-6 详解ES6执行时词法环境/作用域/执行上下文/执行栈和闭包

https://262.ecma-international.org ECMA原文地址