首页 > 代码库 > js原型 作用域

js原型 作用域

了解JavaScript原型链之前首先肯定要知道什么是原型。

JavaScript中,原型是一个对象,通过原型可以实现属性的继承。既然原型是一个对象,那么任何一个对象都可以称为原型吗?是,记住它。什么对象有原型?任何对象(undefined,null,boolean,number,string是主类型,不是对象)默认情况下都有一个原型,但是原型也是一个对象,所以对象的原型也有原型,记住,下面有用。

js中的对象中都包含一个指向原型对象的指针,但是是不能被直接访问的,为了方便的看到原型,chrome和Firefox提供了__proto__属性,但这只是浏览器提供的,换个浏览器就有可能不提供这个属性,ECMA引入了标准的原型访问器Object.getPrototype(object)。
这里又说到了ECMA解释一下和JavaScript的区别。ECMAScript是一种标准,而JavaScript可以理解成一种集合,各个浏览器基于这个标准实现各自的JavaScript(不同的浏览器对于同一段代码可能执行不通的结果),JavaScript其实是由三个不同的部分组成:核心(ECMAScript)、文档对象模型(DOM)、浏览器对象模型(BOM)。不多撤了。

查看一个对象的原型,在浏览器控制台上输入

function Human(name){
    this.name = name;
    
    this.getName = function(){
        console.log("i am " + this.name);
    };
}
var h = new Human("fzk");
h;

技术分享

会看到__proto__属性,可以看到h有一个原型属性,这个原型有一个constructor属性,上面说了,原型也是一个对象,所以也有一个__proto__原型对象。constructor是一个函数类型对象,是对象实例化时的函数。对象实例化(new)到底是一个什么过程?其实就是干了四件事

new Human("fzk") = {
  var obj = {};
  obj.__proto__ = Human.prototype;
  var result = Human.call(obj,"fzk");
  return typeof result === ‘obj‘? result : obj;
}

(1)创建一个空对象obj;
(2)把obj的__proto__ 指向Human的原型对象prototype,此时便建立了obj对象的原型链:obj->Human.prototype->Object.prototype->null
(3)在obj对象的执行环境调用Human函数并传递参数“fzk”。 相当于var result = obj.Human("fzk")。当这句执行完之后,obj便产生了属性name并赋值为"fzk"。
(4)考察第3步返回的返回值,如果无返回值或者返回一个非对象值,则将obj返回作为新对象;否则会将返回值作为新对象返回。

上面再创建的过程中,会把构造函数指向原型的constructor。

了解上面的过程,下面的代码就很好理解了。

h.__proto__ === Human.prototype
Human.prototype.__proto__ === Object.prototype
Person.prototype.constructor === Person

这里又看到了prototype,这里说明一下prototype和__proto__
1.所有的对象都有一个__proto__属性
2.对于函数对象(Function,由function(){}结构定义出来)都另外含有一个prototype属性。

JavaScript中,万物皆对象。Function函数对象也是一个对象,它也有原型。这里Human(Function对象)的作用域链是什么样的

技术分享

可以看出:Human->Function.prototype->Object.prototype->null

总之最终都会指向Object。上面说到过函数对象才会有prototype属性,为什么这里(不止这里,上面也用到了)Object也有prototype属性。

typeof Object
> "function"

控制台上敲一下上面的代码,会发现Object是一个function。

最终看一下:

 

解释了这么多,最终看一下官方定义:ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
刚开始看是不是超级迷糊?是不是怀疑人生?但是通过例子仔细理解一下,果然ECMAScript说出来的话就是言简意赅。JavaScript也就是利用原型链实现了继承。
我们了解原型,肯定是想更好的使用它,在什么情况下会使用到原型?我们首先要知道原型有哪些特点,
1.根据官方定义,实现继承肯定得用这个了。但是基本上不用我们自己来实现,除非你要手动去修改对象的原型指针。
2.原型中定义的属性,是可以被所有实例共享的(为什么?因为实例化的时候已经将函数对象的prototype赋值给了实例对象的__proto__,对象会根据这个原型链查找属性最终在函数对象的prototype属性定义的属性都会通过原型链被实例找到,但是注意,这里能找到是因为通过原型链查找而不是实例对象具有这个属性,是不是想到了多个实例化对象可以共用一个内存空间,实际情况也确实是这样。这种情况下你在仔细考虑考虑,new的时候是不是其实相当于实例对象继承了原函数对象的属性和方法,JavaScript里实例化所要表达的意思其实是继承。撤多了,下面会继续说这个事),可以减少内存使用。而且还可以动态的添加方法。

经过上面两点分析也就知道了原型的使用情况,1.继承、2.多个实例对象共用一块内存、3.动态给所有实例添加方法。

继续撤上面括号中说的JavaScript中new的意义。JavaScript说万物皆对象,为什么还要通过new来创建对象,画蛇添足?仔细研究一下,JavaScript说,A instanceof B === true,那么A就是B的实例,而这个instanceof是怎么判断的?就像下面这样:

var L = A.__proto__;
var R = B.prototype;
 if(L === R)
return true;

是不是感觉好巧,就是这么巧,在实例化h的时候,就是将Human的prototype给了h的__proto__。h又继承了原函数对象的属性,因此,new的作用是为了让新做出来的对象具有原对象的属性和方法。没理解?就是让{}.__proto__ = Human.prototype,这时{}是一个对象并且也是Human的实例,但是现在{}什么属性也没有。通过new出来的对象h就具有了Human的属性和方法,并且h还是Human的实例。所以说,new存在JavaScript的意义不是穿件一个对象,而是为了实现JavaScript的继承。

原型链理解的差不多了,看一下下面的输出是否全能理解

function Animal(name){
    this.name = name;
  }
  Animal.color = "black";
  Animal.prototype.say = function(){
    console.log("I‘m " + this.name);
  };
  var cat = new Animal("cat");

  console.log(
    cat.name, //cat
    cat.height //undefined
  );
  cat.say(); //I‘m cat

  console.log(
    Animal.name, //Animal
    Animal.color //back
  );
  Animal.say(); //Animal.say is not a function

看完原型链,接下来看一下作用域链。
JavaScript的作用域是非块级作用域,是函数级作用域。什么意思?也就是说,if、for、while....后面的大括号并不能隔离作用域,这些大括号里定义的属性与在大括号外面的定义的属性是一个性质的。看一下经典的例子

var data =http://www.mamicode.com/ [];
for(var i = 0 ; i < 3; i++){
    data[i]=function() {
        console.log(i);
    }
// data[0](); 三次分别打印 0、1、2 } data[
0]();// 3 data[1]();// 3 data[2]();// 3

理解了非块级作用域,这个也就很好理解了。在这里,i 就相当于全局作用域的变量,data数组里的数据就是指向打印 i 的函数指针,函数的功能是要打印 i,循环结束后,i已经变成了3,这时在打印 i当然打印出来的是3。
这里又引出了一个全局作用域的概念,与之对应的是局部作用域。全局作用域是指对象可以在代码的任何地方都可以访问,最外层定义的变量、没有var修饰的变量、window对象的属性 都是全局作用域的变量。局部作用域中定义变量只有在函数内能使用。

说到作用域,肯定就要说闭包。什么是闭包,所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。我的理解就是一个函数内可以使用另一个函数中定义的属性,这些属性就会被闭包。

function count() {
    var x = 0;
    
    return {
        increase: function increase() { return ++x; },
        decrease: function decrease() { return --x; }
    };
}
c = count();
c.increase();

闭包和作用域链就有关系了。每个执行环境都有用来存放变量函数参数等信息的VO,当从当前执行环境找不到变量的时候会向上继续查找。这样就形成了一个作用域链,这个闭包的属性就是那些从当前环境没找到这个变量,从上级的某层中找到这个变量,但是这个变量还不是全局作用域的变量。正常的没有闭包的函数在函数执行完后,所有的属性变量等会被销毁,但是闭包中的变量在函数执行完后是不会被销毁的。所以用闭包就需要注意内存泄漏的问题。

闭包应用的场景:
1、保护函数内的变量安全。x只能通过count()访问到,此外,再也没有办法能访问到了。
2、保存数据。这个应用的地方就多了,计数器、数据收集。。。

为什么写了这么一大骗文章,就是想说最后一个事情,JavaScript在既有继承又有闭包的情况下查找变量时是怎么找的。首先,会通过作用域链,找到这个属性是属于哪个对象的,然后在通过作用域链向上查找。

 

参考:

http://www.jb51.net/article/78709.htm

http://www.cnblogs.com/wilber2013/p/4909459.html

http://www.cnblogs.com/wilber2013/p/4924309.html

js原型 作用域