目录
一、前言
本次博客将我在JS学习中的一些难点进行汇总
二、正文
(1).原型
四个规则
1、引用类型,都具有对象特性,即可自由扩展属性。
2、引用类型,都有一个隐式原型 __proto__
属性,属性值是一个普通的对象。
3、引用类型,隐式原型 __proto__
的属性值指向它的构造函数的显式原型 prototype
属性值。
4、当你试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型 __proto__
(也就是它的构造函数的显式原型 prototype
)中寻找。
引用类型:Object,Array,Function,Date,RegExp
规则详解
规则一
引用类型都具有对象的特性(自由扩展属性)
const obj = {}
const arr = []
const fn = function () {}
obj.a = 1
arr.a = 1
fn.a = 1
console.log(obj.a) // 1
console.log(arr.a) // 1
console.log(fn.a) // 1
规则二
引用类型都有一个隐式原型__proto__
属性,属性值是一个普通对象。
const obj = {};
const arr = [];
const fn = function() {}
console.log('obj.__proto__', obj.__proto__);
console.log('arr.__proto__', arr.__proto__);
console.log('fn.__proto__', fn.__proto__);
规则三
引用类性的隐式原型__proto__
属性都指向它构造函数的显式原型prototype
的属性值:
const obj = {};
const arr = [];
const fn = function() {}
obj.__proto__ == Object.prototype // true
arr.__proto__ === Array.prototype // true
fn.__proto__ == Function.prototype // true
规则四
当想要访问某个对象的属性的时候,如果对象本身没有此属性那么就会去它的隐式原型__proto__
(同时也是它的构造函数的显式原型protype
里面去找)
const obj={a:1}
obj.toString
console.log(obj.toString)//ƒ toString() { [native code] }
obj
对象并没有 toString
属性,之所以能获取到 toString
属性,是遵循了第四条规则,从它的构造函数 Object
的 prototype
里去获取。
原型链
例:
function Person(name) {
this.name = name
return this // 其实这行可以不写,默认返回 this 对象
}
var nick = new Person("nick")
nick.toString
// ƒ toString() { [native code] }
实例化的对象nick
之所以能够找到toString
是因为原型链。
首先nick
在自身找不到toString
方法,然后就向上找Person
的构造函数的prototype
还是没有找到,由于构造函数的protype
也是一个对象,那么protype
的构造函数就是Object
所以就找到了Object.protype
下的toString
方法
console.log(Person.prototype.__proto__===Object.prototype)//true
总结
最后指向的null
是为了防止死循环,Object.prototype
的隐式原型指向 null
。
instanceof
instanceof
运算符用于测试构造函数的 prototype
属性是否出现在对象原型链中的任何位置。 instanceof
的简易手写版,如下所示:
// 变量R的原型 存在于 变量L的原型链上
function instance_of (L, R) {
// 验证如果为基本数据类型,就直接返回 false
const baseType = ['string', 'number', 'boolean', 'undefined', 'symbol']
if(baseType.includes(typeof(L))) { return false }
let RP = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null) { // 找到最顶层
return false;
}
if (L === RP) { // 严格相等
return true;
}
L = L.__proto__; // 没找到继续向上一层原型链查找
}
}
function Foo(name) {
this.name = name;
}
var f = new Foo('nick')
f instanceof Foo // true
f instanceof Object // true
分析流程:
1、 f instanceof Foo
: f
的隐式原型 __proto__
和 Foo.prototype
,是相等的,所以返回 true
。
2、 f instanceof Object
: f
的隐式原型 __proto__
,和 Object.prototype
不等,所以继续往上走。 f
的隐式原型 __proto__
指向 Foo.prototype
,所以继续用 Foo.prototype.__proto__
去对比 Object.prototype
,这会儿就相等了,因为 Foo.prototype
就是一个普通的对象。
(2)this指向
执行上下文
执行上下文中,包含了变量环境,词法环境,外部环境,this
this 是和执行上下文绑定的,也就是说每个执行上下文都有一个this。
执行上下文的概念
执行上下文是评估和执行 JavaScript 代码的环境的抽象概念,当 JavaSciprt 代码在运行时,其运行在执行上下文中。JavaScript 中有三种执行上下文类型:
全局执行上下文: 任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
函数执行上下文: 当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
eval函数执行上下文: 执行在eval函数中的代码会有属于它自己的执行上下文。
由于eval函数执行上下文用的不多,所以这里只介绍全局执行上下文和函数执行上下文:
在全局执行上下文中,this 是指向 window 对象的;
在函数执行上下文中,默认情况下调用一个函数,其执行上下文的 this 也是指向 window 的。
执行上下文栈
浏览器中的JS解释器为单线程,也就是说浏览器同⼀时间只能做⼀个事情。代码中只有⼀个全局执⾏上下⽂和⽆数个函数执⾏上下⽂,这些组成了执⾏上下⽂栈(Execution Stack)。 ⼀个函数的执⾏上下⽂,在函数执⾏完毕后,会被移出执⾏上下⽂栈。
比如:
function c(){
console.log('ok');
}
function a(){
function b(){
c();
}
b();
}
a();
执行上下文的栈是这样:
函数的this指向
this是JS中的关键字,多数情况下this指向调用它的对象
首先 this 指向的应该是一个对象(函数执行上下文对象)。其次,这个对象指向的是调用它的对象,如果调用它的不是对象或对象不存在,则会指向全局对象(严格模式下为 undefined)。
其实,this 是在函数被调用时确定的,它的指向取决于函数调用的地方,而不是它被声明的地方(除箭头函数外)。当函数被调用时,会创建一个执行上下文,它包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息,this 就是这个记录的一个属性,它会在函数执行的过程中被用到。
this 在函数的指向绑定形式有四种:默认绑定、隐式绑定、显式绑定、new绑定。
默认绑定(全局情况)
函数在浏览器全局环境中直接使用不带任何修饰的函数引用进行调用,非严格模式下 this
指向 window
;在 use strict
指明严格模式的情况下就是 undefined
(严格模式不允许 this 指向全局对象)。
function fn1 () {
console.log(this)
}
function fn2 () {
'use strict'
console.log(this)
}
fn1() // window
fn2() // undefined
var num = 1
var foo = {
num: 10,
fn: function() {
console.log(this)
console.log(this.num)
}
}
var fn1 = foo.fn
fn1()//window,1
输出window和1这里 this
仍然指向 window
。虽然 fn
函数在 foo
对象中作为方法被引用,但是在赋值给 fn1
之后,fn1
的执行仍然是在 window
全局环境中。因此输出 window
和 1
,它们相当于:
console.log(window)
console.log(window.num)
隐式绑定(上下文对象)
如果函数在某个上下文对象中调用,那么 this 绑定的是那个上下文对象。
var a = 'hello'
var obj = {
a: 'world',
fn: function() {
console.log(this.a)
}
}
obj.fn()
最后输出为world
这里fn方法是作为对象的属性调用的,此时fn方法执行时,this会指向obj对象。也就是说,此时this指向的是调用这个方法的对象。
嵌套多层对象时
const obj1 = {
text: 1,
fn: function() {
return this.text
}
}
const obj2 = {
text: 2,
fn: function() {
return obj1.fn()
}
}
const obj3 = {
text: 3,
fn: function() {
var fn = obj1.fn
return fn()
}
}
console.log(obj1.fn())
console.log(obj2.fn())
console.log(obj3.fn())
第一个 console
输出 1
,这时 this 指向了调用 fn 方法的对象 obj1,所以会输出obj1中的属性 text
的值 1
;
第二个 console
输出 1
,这里调用了 obj2.fn()
,最终还是调用 o1.fn()
,因此仍然会输出 1
。
第二个 console
输出 undefined
,在进行 var fn = o1.fn
赋值之后,是直接调用的,因此这里的 this
指向 window
,答案是 undefined
。
使最后的console.log(obj2.fn())
输出2:
const obj1 = {
text: 1,
fn: function() {
return this.text
}
}
const obj2 = {
text: 2,
fn: o1.fn
}
console.log(obj2.fn())
虽然调用了obj1.fn,但是最后还是调用了obj2,相当于提前进行了赋值
显示绑定(apply,bind,call)
显式绑定是指需要引用一个对象时进行强制绑定调用,显式绑定可以使用apply、call、bind
方法来绑定this
值,使其指向我们指定的对象。
call、apply 和 bind三个方法都可以改变函数 this
指向,但是 call 和 apply 是直接进行函数调用;bind
不会执行函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的 this
指向,需要我们手动调用。call 和 apply 的区别: call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组。
三个方法的使用:
const target = {}
fn.call(target, 'arg1', 'arg2')
fn.apply(target, ['arg1', 'arg2'])
fn.bind(target, 'arg1', 'arg2')()
如果把 null 或 undefined 作为 this 的绑定对象传入 call、apply、bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:
var a = 'hello'
function fn() {
console.log(this.a)
}
fn.call(null)
输出的是"hello"因为传入了null
使用的是默认的绑定规则,(this.a)
相当于window.a
所以输出hello
const foo = {
name: 'hello',
logName: function() {
console.log(this.name)
}
}
const bar = {
name: 'world'
}
console.log(foo.logName.call(bar))
最后输出的是"hello",call中把bar传入,相当于this=bar,bar.name
为world
多次bind
let a = {}
let fn = function () {
console.log(this)
}
fn.bind().bind(a)()
输出为window而不是a
其中fn.bind().bind(a)
相当于
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()
不管给函数 bind
几次,fn
中的 this
永远由第一次 bind
决定,所以结果永远是 window
。
new绑定(构造函数)
函数作为构造函数使用 new 调用时, this 绑定的是新创建的构造函数的实例:
function Person(name,age){
this.name = name;
this.age = age;
this.say = function(){
console.log(this.name + ":" + this.age);
}
}
var person = new Person("CUGGZ",18);
console.log(person.name); // CUGGZ
console.log(person.age); // 18
person.say(); // CUGGZ:18
this 就指向了构造函数 Person 的新对象person,所以使用 this 可以获取到 person 对象的属性和方法。
在使用new
调用构造函数的时候,执行这些操作:
创建一个新对象;
构造函数的 prototype 被赋值给这个新对象的 proto;
将新对象赋给当前的 this;
执行构造函数;
this优先级
使用this的优先级:new绑定>显式绑定>隐式绑定>默认绑定
function foo (a) {
console.log(this.a)
}
const obj1 = {
a: 1,
foo: foo
}
const obj2 = {
a: 2,
foo: foo
}
obj1.foo.call(obj2)//2
obj2.foo.call(obj1)//1
根据显式绑定>隐式绑定,所以指向的this分别为call中传入的obj2和obj1。
function foo (a) {
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)//2
先显式绑定了obj1,然后bar(2)
给它赋值。
this
指向的整个流程图
特殊的this指向
箭头函数
箭头函数根据声明的位置来指向this
const foo = {
fn: function () {
setTimeout(function() {
console.log(this)
})
}
}
console.log(foo.fn())
// window
这里,this
出现在 setTimeout()
中的回调函数里,因此 this
指向 window
对象。如果需要 this
指向 foo
这个 object 对象,可以使用箭头函数解决:
const foo = {
fn: function () {
setTimeout(() => {
console.log(this)
})
}
}
console.log(foo.fn())
// {fn: ƒ}
因为setTimeout在fn中声明
,所以this指向fn
由于箭头函数没有 this
,箭头函数中的 this
只取决包裹箭头函数的第一个普通函数的 this
。在这个例子中,因为包裹箭头函数的第一个普通函数是 a
,所以此时的 this
是 window
。
需要注意,箭头函数的 this 绑定是无法通过 call、apply、bind 方法修改的。且因为箭头函数没有构造函数 constructor,所以也不可以使用 new 调用,即不能作为构造函数,否则会报错。
数组方法
在数组arr的forEach
回调函数中的this
var obj = {
arr: [1]
}
obj.arr.forEach(function() {
console.log(this)
}) //window
this指向window
forEach()语法:
array.forEach(function(currentValue, index, arr), thisValue)
1)function(currentValue, index, arr):必需。 数组中每个元素需要调用的函数。
currentValue:必需,当前元素
index:可选,当前元素的索引值
arr:可选,当前元素所属的数组对象
2)thisValue:可选,传递给函数的值一般用 "this" 值。如果这个参数为空, "undefined" 会传递给 "this" 值。
可以看到,forEach方法有两个参数,第一个是回调函数,第二个是 this 指向的对象,这里只传入了回调函数,第二个参数没有传入,默认为 undefined,所以会输出全局对象。
除了forEach方法,需要传入 this 指向的函数还有:every()、find()、findIndex()、map()、some(),在使用的时候需要注意。
立即执行函数
立即执行函数就是定义后立刻调用的匿名函数:
var name = 'hello'
var obj = {
name: 'world',
sayHello: function() {
console.log(this.name)
},
hello: function() {
(function(cb) {
cb()
})(this.sayHello)
}
}
obj.hello() // hello
执行结果是 hello,是 window.name 的值。立即执行函数作为一个匿名函数,通常就是直接调用,而不会通过属性访问器(obj.fn)的形式来给它指定一个所在对象,所以它的 this 是确定的,就是默认的全局对象 window。
setTimeOut和setInterval
var name = 'hello'
var obj = {
name: 'world',
hello: function() {
setTimeout(function() {
console.log(this.name)
})
}
}
obj.hello() // hello
this.name 是在 obj.hello () 里被调用的,结果却输出了 window.name。其实,延时效果(setTimeout)和定时效果(setInterval)都是在全局作用域下实现的。无论是 setTimeout 还是 setInterval 里传入的函数,都会首先被交到全局对象手上。因此,函数中 this 的值,会被自动指向 window。
(3)闭包
上级作用域
函数在哪里创建的,哪里就是上级作用域
var a = 10
function foo(){
console.log(a)
}
function sum() {
var a = 20
foo()
}
sum()
/* 输出
10
/
这里foo()是在全局创建的,所以上级作用域为window
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
f()
return f
}
var x = fn()
x()
x()
console.log(n)
/* 输出
* 21
22
23
10
/
f()
的上级作用域为fn()
,实际x()
调用的是f()
里面的n为fn()
中的n
JS堆栈内存释放
堆内存:存储引用类型值,对象类型就是键值对,函数就是代码字符串。
堆内存释放:将引用类型的空间地址变量赋值成 null
,或没有变量占用堆内存了浏览器就会释放掉这个地址
栈内存:提供代码执行的环境和存储基本类型值。
栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉。
但栈内存的释放也有特殊情况:① 函数执行完,但是函数的私有作用域内有内容被栈外的变量还在使用的,栈内存就不能释放里面的基本值也就不会被释放。② 全局下的栈内存只有页面被关闭的时候才会被释放
闭包
在 JS 忍者秘籍(P90)中对闭包的定义:闭包允许函数访问并操作函数外部的变量。红宝书上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。 MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。这里的自由变量是外部函数作用域中的变量。
概述上面的话,闭包是指有权访问另一个函数作用域中变量的函数
闭包形成原因
内部的函数存在外部作用域的引用就会导致闭包。
var a = 0
function foo(){
var b =14
function fo(){
console.log(a, b)
}
fo()
}
foo()//0,14
闭包变量存储的位置
闭包中的变量存储的位置是堆内存。
假如闭包中的变量存储在栈内存中,那么栈的回收 会把处于栈顶的变量自动回收。所以闭包中的变量如果处于栈中那么变量被销毁后,闭包中的变量就没有了。所以闭包引用的变量是出于堆内存中的。
闭包的作用
保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化
闭包使用场景
return
回一个函数
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
return f
}
var x = fn()
x() // 21
函数作为参数
var a = '林一一'
function foo(){
var a = 'foo'
function fo(){
console.log(a)
}
return fo
}
function f(p){
var a = 'f'
p()
}
f(foo())
/* 输出
* foo
/
IIFE(自执行函数)
var n = '林一一';
(function p(){
console.log(n)
})()
/* 输出
* 林一一
/
循环赋值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
因为存在闭包的原因上面能依次输出1~10,闭包形成了10个互不干扰的私有作用域。将外层的自执行函数去掉后就不存在外部作用域的引用了,输出的结果就是连续的 10。为什么会连续输出10,因为 JS 是单线程的遇到异步的代码不会先执行(会入栈),等到同步的代码执行完 i++
到 10时,异步代码才开始执行此时的 i=10
输出的都是 10。
使用回调函数就是闭包
window.name = '林一一'
setTimeout(function timeHandler(){
console.log(window.name);
}, 100)
例题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
//输出3,3,3
i是全局变量,在i变成3之后再进行data[1,2,3]的调用
可以使用闭包达到预期的效果,首先是自执行函数和闭包
var data = [];
for (var i = 0; i < 3; i++) {
(function(j){
setTimeout( data[j] = function () {
console.log(j);
}, 0)
})(i)
}
data[0]();
data[1]();
data[2]()
//0,1,2
使用let
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
(4)单线程和异步
单线程
单线程指在JavaScript引擎中负责解释和运行的线程唯一,同一时间只能执行一个任务。
JavaScript为什么要引入单线程?
因为浏览器需要渲染DOM,JavaScript可以修改DOM,同时,在执行JavaScript时浏览器会停止渲染DOM,如果JavaScript不是单线程的话,同时执行多段JavaScript,如果多段JavaScript都涉及到了修改DOM,那么会出现DOM冲突。
为了避免DOM冲突,可以引入锁的方法来解决冲突,但是提高了复杂性,所以JavaScript就引入了单线程
单线程实现比较简单,而且执行的环境相对比较单纯。
引入单线程的局限
引入单线程意味着在一个线程内,只允许执行一个任务,一旦某个任务执行事件过长,那么后面的任务只能在后面等待,这样会拖延整个程序的执行。
浏览器的无响应一般是因为某个任务执行时间过长,导致整个页面卡住,其他任务无法执行。
为了解决此局限性,JavaScript引入了同步和异步的概念。
同步和异步
同步
func(args...)
如果在函数返回时,执行者可以立即得到函数的返回值,那么这就是同步。
异步
如果在函数返回时,执行者不能立即得到返回的值,而是需要一定手段来得到返回值,那么它就是异步。
采用异步的原因:
JavaScript是单线程
提高CPU利用率
异步的过程
fs.readFile('data.json', 'utf8', function(err, data) {
console.log(data)
})
在执行readfile
后,不会立刻打印data
而是在读取完data.json
后才会打印。
也就是异步函数 fs.readFile
执行很快,但后面还有工作线程执行异步任务、通知主线程、主线程回调等操作,这个过程就叫做异步过程。
主线程发起一个异步操作,相应的工作线程接受请求并告知主线程已收到(异步函数返回);主线程继续执行后面的任务,同时工作线程执行异步任务;工作线程完成任务后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数)。
消息队列和事件循环
工作线程(子线程)把返回的消息放到消息队列,然后主线程在事件循环中得到消息
消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。
事件循环(event loop)
主线程从事件循环中不断的取消息,执行消息,此过程称为事件循环,这样的机制叫做事件循环机制,一次取消息并且执行的过程叫做一次循环
大概的实现过程:
while(true) {
var message = queue.get()
execute(message)
}
事件循环是JavaScript实现异步的具体解决方案,其中同步代码,直接执行;异步函数先放在异步队列中,待同步函数执行完毕后,轮询执行 异步队列 的回调函数。
消息队列
消息队列其中的消息就是子线程在注册异步任务时的回调函数。比如:
$.ajax('XXX', function(res) {
console.log(res)
})
...
主线程在发出ajax请求后,继续执行其他任务,而子线程负责ajax请求,在拿到请求后会将其封装成JavaScript对象,然后构造一条消息:
// 消息队列里的消息
var message = function () {
callback(response)
}
其中callback
就是ajax网络请求成功的回调函数
主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是 message
函数),并执行它。到此为止,就完成了工作线程对主线程的 通知
,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX 线程在收到 HTTP 响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。
整个流程的执行流程图:
异步过程中的回调函数,一定不在当前这一轮事件循环中执行
生产者和消费者
生产者和消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加数据,消费者从存储空间中取走数据,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
从生产者与消费者的角度看,异步过程是这样的:
工作线程是生产者,主线程是消费者(只有一个消费者)。工作线程执行异步任务,执行完成后把对应的回调函数封装成一条消息放到消息队列中;主线程不断地从消息队列中取消息并执行,当消息队列空时主线程阻塞,直到消息队列再次非空。
异步的实现方式
ES6之前:callback、eventloop、Promise
ES6:Generator
ES7:Async/Await