JS的三座大山 JavaScript的三座大山

1.JS三座大山之原型和原型链

构造函数

提到原型就不得不说一下构造函数,我们先来看一个简单的构造函数的例子:

function  Foo (name,age) {
    this.name = name;
    this.age = age;
    //return  this;
}
var f = new Foo ("zhangsan",20);
var f1 = new Foo ("lisi",21);          //创建多个对象

需要说明的是:① 构造函数需以大写字母开头,这样提高了代码的易读性;② return this 那句可有可无,因为它是默认存在的。 在上面的例子中,我们首先创建了一个空对象,再让this指向这个对象,然后执行代码,即给this.name赋值,最后返回this,并将值赋给f和f1。这其实也是new一个对象的过程。 在这里我们稍稍拓展一下构造函数的知识: ① 所有的引用类型都有构造函数 var a = {}其实是var a = new object()的语法糖 var b = []其实是var b = new Array()的语法糖 function Foo() {....}其实是var Foo = new Function()的语法糖 在实际写代码中,我们更推荐使用前者的写法 ② 可以使用instanceof判断一个函数是否是一个变量的构造函数,例如

var a = new Array()
alert (a instanceof Array)    // true

同时alert (a instanceof Object)也会返回true,这是因为Array是object的子类。再如

function Test () {}
var a = new Test()
alert (a instanceof Test)      // true 

什么是原型呢?

原型就是一个普通的对象,每个对象都有一个原型(Object除外),原型能存储我们的方法,构造函数创建出来的实例对象能够引用原型中的方法。

Javascript中所有的对象都是Object的实例,并继承Object.prototype的属性和方法,有些属性是隐藏的。换句话说,在对象创建时会存在预定义的属性,其中有一个属性就是原型对象。 下面,我们来说说原型规则: ① 所有的引用类型(数组,对象,函数)都具有对象特性,即可自由扩展属性(除了“null”)

var obj = {};
obj.a = 100;
var arr = [];
arr.a = 100;
function fn1 () {}
fn1.a = 100;

② 所有的引用类型(数组,对象,函数)都有一个_proto_(隐式原型)属性,属性值也是一个普通的对象

console.log(obj.__proto__);
console.log(arr.__proto__);
console.log(fn1.__proto__);

③ 所有的函数,都有 一个prototype(显式原型)属性,属性值也是一个普通的对象

console.log(fn1.prototype);

④ 所有引用类型(数组,对象,函数)的_proto_属性值指向它的构造函数的prototype属性值

console.log(obj.__proto__ === Object.prototype);        
console.log(arr.__proto__ === Array.prototype);   
console.log(fn1.__proto__ === Function.prototype);    

⑤ 当试图得到一个对象的属性时,如果这个对象本身没有这个属性,那么就会去它的_proto_(即构造函数的prototype)中寻找

//构造函数
function Foo(name) {
    this.name = name;
}
Foo.prototype.alertName = function () {
    alert(this.name);
}
//创建实例
var f = new Foo("xiaohui");
f.printName = function () {
    console.log(this.name);
}
//测试
f.printName();
f.alertName();

就刚才的例子,如果想要循环得到对象本身的属性可以通过for in 实现

var item;
for (item in f) {
    //高级浏览器已经在for in中屏蔽了来自原型的属性
    //但是还是加上判断,保证程序的健壮性
    if (f.hasOwnProperty(item)) {
        console.log (item);
    }
} 

如果现在我想要得到f.toString(),我们会发现f和Foo都没有这个方法,于是就去f._proto._proto(Foo.prototype._proto)中寻找,此时找到并调用。

如图,我们要找f的一个属性时首先会在f._proto中寻找,因为f._proto的属性值是一个对象,所以也有_proto属性,如果没找到,就会去f._proto._proto中找,同理,如果还没找到就继续向上,直到结果为null为止。这个由_proto串起来的直到Object.prototype._proto为null的链就是原型链。 以前一般使用对象的 _proto_属性,ES6推出后,推荐用Object.getPrototypeOf()方法来获取对象的原型 :给定对象的原型。如果没有继承属性,则返回null。

2. JS三座大山之作用域和闭包

执行上下文 在讲作用域之前我们来说一个知识点:执行上下文。每一个函数都有自己的执行上下文EC(执行环境 execution context),并且每个执行上下文中都有它自己的变量对象VO(Variable object),用于存储执行上下文中的变量 、函数声明 、函数参数,这解释了js如何找到我们定义的函数和变量。并且函数是js中唯一一个能创建出作用域的,注意:for,if()else()不能创建作用域。看一段代码

var a = 10

此时打开控制台就会打印undefined,这是因为在代码还没执行之前,a会被提前声明,此时的代码可以理解为 现在有了let 后可以用let 不会变量提升

var a 
console.log (a)      // undefined
var a = 10

代码执行到console.log (a) 时a只声明了,并没有赋值,所以结果当然是undefined。再来看一段代码

fn ("zhangsan",20)
function fn (name) {
    age = 20
    console.log (name,age)      // "zhangsan" 20
    var age
}

此时打印的结果就是“zhangsan”,20。我们可以将代码理解为

function fn (name) {
    var age
    age = 20
    console.log (name,age)        // "zhangsan" 20
}
fn ("zhangsan",20)

一段或者一个函数都会生成执行上下文,针对一段会生成全局的执行上下文,在执行之前先将变量定义和函数声明提前,针对一个函数会生成一个函数执行上下文,在函数执行之前,将函数中的变量定义,函数声明,this,arguments拿出来。 要注意的是函数声明和函数表达式的区别,看代码

function fn () { }          // 函数的声明
var fn = function () { }    // 函数表达式

###this 本质上来说,在 js 里 this 是一个指向函数执行环境的指针。this 永远指向最后调用它的对象,并且在执行时才能获取值,定义是无法确认他的值。

var a = {
    name :  "A",
    fn : function () {
        console.log (this.name)
    }
}
a.fn()                          // this === a
a.fn.call ({name : "B"})        // this === {name : "B"},
var fn1 = a.fn
fn1()                           // this === window

第一种情况,是a调用了fn(),所以此时的this为a;第二种情况,使用call(),将this的值指定为了{name : "B"};第三种情况,把a.fn赋给了fn1,但是调用时却是window在调用,所以this === window

作为构造函数执行

function  Foo (name,age) {
    this.name = name           // this === f
    this.age = age             // this === f
    //return  this
}
var f = new Foo ("zhangsan",20)       

在这里new操作符会改变函数this的指向问题,指向创建的对象f。为什么this会指向f?首先new关键字会创建一个空的对象,然后会自动调用一个函数apply方法,将this指向这个空对象,这样的话函数内部的this就会被这个空的对象替代。 更新一个小问题:若构造函数中返回this或返回值是基本类型(number、string、boolean、null、undefined)的值,则返回新实例对象;若返回值是引用类型的值,则实际返回值为这个引用类型。例如:

作为普通函数执行

function  fn () {
   console.log (this)       // this === window
}
fn ()

作为对象属性执行

var obj = {
    name : "A",
    printName : function () {
        console.log (this.name)        // this === obj
    }
}
obj.printName ()

.call(),apply(),bind() 三个函数可以修改 this 的指向,具体请往下看:

var name  = "小明" , age = "17"
var obj = {
    name : "安妮",
    objAge :this.age,
    fun : function () {
        console.log ( this.name + "今年" + this.age )    
    }
}
console.log(obj.objAge)          // 17
obj.fun()                        // 安妮今年undefined
var name  = "小明" , age = "17"
var obj = {
  name : "安妮",
  objAge :this.age,
  fun : function (like,dislike) {
    console.log (this.name + "今年" + this.age ,"喜欢吃" + like + "不喜欢吃" + dislike)   
  }
}

var a = {
    name : "Jay",
    age : 23
}

obj.fun.call(a,"苹果","香蕉")              // Jay今年23 喜欢吃苹果不喜欢吃香蕉
obj.fun.apply(a,["苹果","香蕉"])           // Jay今年23 喜欢吃苹果不喜欢吃香蕉
obj.fun.bind(a,"苹果","香蕉")()            // Jay今年23 喜欢吃苹果不喜欢吃香蕉

首先 call,apply,bind 第一个参数都是 this 指向的对象,call 和 apply 如果第一个参数指向 null 或 undefined 时,那么 this 会指向 windows 对象。

call,apply,bind 的执行方式如上例所示。call,apply 都是改变上下文中的 this,并且是立即执行的。bind 方法可以让对应的函数想什么时候调用就什么时候调用。

###闭包 专业说法:当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。(对内部函数的引用会添加到外部函数的活动对象上)

还可以这么理解:闭包就是一个具有封闭功能与包裹功能的结构,是为了实现具有私有访问空间的函数的,函数可以构成闭包,因为函数内部定义的数据函数外部无法访问,即函数具有封闭性;函数可以封装代码即具有包裹性,所以函数可以构成闭包。创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量。

而大家普遍认为的闭包是:

(function fn() {})();

闭包有3个特性: 1、函数嵌套函数 2、函数内部可以引用外部的参数和变量

function f1() {
   var n=999;
   function f2() {
       alert(n);    // 999
   }
}

f2被包在函数f1内部,f1内的所有局部变量,对f2都是可见的。反过来就不行,f2内的局部变量,对f1就是不可见的。 3、参数和变量不会被垃圾回收机制回收,让这些变量的值始终保持在内存中。 闭包有什么用,使用场景: 当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会“污染”全局的变量时,就可以用闭包来定义这个模块。

使用闭包的注意点: 1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。 2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

3.JS三座大山之异步和单线程

我们先感受下异步。

        console.log("start");
        setTimeout(function () {
            console.log("setTimeout");
        }, 1000);
        console.log("end");

使用异步后,打印的顺序为 start-> end->medium。因为没有阻塞。

为什么会产生异步呢?

首先因为 js 为单线程,也就是说 CPU 同一时间只能处理一个事务。得按顺序,一个一个处理。

如上例所示,第一步:执行第一行打印 “start”;第二步:执行 setTimeout,将其中的函数分存起来,等待时间结束后执行;第三步:执行最后一行,打印 “end”;第四部:处于空闲状态,查看暂存中,是否有可执行的函数;第五步:执行分存函数。

为什么 js 引擎是单线程?

js 的主要用途是与用户互动,以及操作 DOM,这决定它只能是单线程。例:一个线程要添加 DOM 节点,一个线程要删减 DOM 节点,容易造成分歧。

为了更好使用多 CPU,H5 提供了 web Worker 标准,允许 js 创建多线程,但是子线程受到主线程控制,而且不得操作 DOM。

任务列队

单线程就意味着,所有的任务都要排队,前一个结束,才会执行后面的任务。如果列队是因为计算量大,CPU 忙不过来,倒也算了。但是更多的时候,CPU 是闲置的,因为 IO 设备处理得很慢,例如 ajax 读取网络数据。js 设计者便想到,主线程完全可以不管 IO 设备,将其挂起,然后执行后面的任务。等后面的任务结束掉,在反过头来处理挂起的任务。

好,我们来梳理一下:

1)所有的同步任务都在主线程上执行,行程一个执行栈。

2)除了主线程之外,还存在一个任务列队,只要一步任务有了运行结果,就在任务列队中植入一个时间。

3)主线程完成所有任务,就会读取列队任务,并将其执行。

4)重复上面三步。

只要主线程空了,就会读取任务列队,这就是 js 的运行机制,也被称为 event loop(事件循环)。

记录你我
请先登录后发表评论
  • latest comments
  • 总共0条评论