js基础(1)--原型与原型链

314 阅读14分钟

1. 变量提升

生成执行环境时,会有两个阶段,创建阶段、代码执行阶段。函数整个放入内存,变量只声明并赋值为 undifned,相同函数会覆盖,函数优于变量提升。

let 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用。

2. bind、call、apply

call 和 apply 都是为了解决改变 this 的指向,只是传参的方式不同。call 接收一个参数列表,apply 只接受一个参数数组。

bind 接收一个参数列表,同样是改变 this 指向,只是会返回一个函数,而 call、apply 是直接执行。

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])

2.1 bind

bind的几种用法:

  1. 创建绑定函数:创建一个函数,不论怎么调用,这个函数都有同样的 this 值。
  2. 偏函数:使一个函数拥有预设的初始参数。
  3. 配合 setTimeout:默认情况下,使用 window.setTimeout() 时,this 关键字会指向 window (或global)对象。显式地把 this 绑定到回调函数,就不会丢失该实例的引用。
  4. 作为构造函数使用的绑定函数:使用bind生成一个新的构造函数,并支持new操作
  5. 快捷调用:用 Array.prototype.slice 来将一个类似于数组的对象(array-like object)转换成一个真正的数组,以下两种方式等价
    1. 使用apply实现
      var slice = Array.prototype.slice;
      
      // ...
      
      slice.apply(arguments);
      
    2. 使用bind实现,这样可以直接使用slice,不需要每次apply
      var unboundSlice = Array.prototype.slice;
      var slice = Function.prototype.apply.bind(unboundSlice);
      //意思是绑定在Array.prototype.slice上的Function.prototype.apply函数
      
      // ...
      
      slice(arguments);
      
      这段代码里面,slice 是 Function.prototype 的 apply() 方法的绑定函数,并且将 Array.prototype 的 slice() 方法作为 this 的值。

bind 是 ES5 引入的,要在 IE9+ 使用,而call、apply是原生就有的,MDN 官方 Polyfill:

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用,而new会改变this指向
          return fToBind.apply(this instanceof fBound
                 ? this
                 : oThis,
                 // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    // 维护原型关系
    if (this.prototype) {
      // 当执行Function.prototype.bind()时, this为Function.prototype 
      // this.prototype(即Function.prototype.prototype)为undefined
      fNOP.prototype = this.prototype; 
    }
    // 下行的代码使fBound.prototype是fNOP的实例,因此
    // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
    fBound.prototype = new fNOP();

    return fBound;
  };
}

bind 实现:

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Not Function')
  }
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

完整解决方案:github.com/Raynos/func…

2.2 call

call 的几种用法:

  1. 调用父构造函数:在一个子构造函数中,你可以通过调用父构造函数的 call 方法来实现继承
  2. 调用匿名函数
  3. 调用函数并且指定上下文的 'this'
  4. 调用函数并且不指定第一个参数(argument)

call 实现

Function.prototype.mycall = function(context) {
    // this 指向调用 mycall 的函数对象,调用 mycall 的若不是函数则报错
    if (typeof this !== 'function') {
        throw new TypeError('Not Function')
    }
    // context 为 null 或者 undefined 或者不传,则指向 window
    context = context || window
    // 将调用 mycall 的函数对象添加到 context 的属性中
    context.fn = this
    // 获取除第一个参数之外的其余参数,然后执行
    const args = [...arguments].slice(1)
    const result = context.fn(...args)
    // 删除该属性
    delete context.fn
    return result
}

我们需要在某个对象上临时调用一个方法,又不能造成属性污染,Symbol是一个很好的选择。

Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
    return undefined; // 用于防止 Function.prototype.myCall() 直接调用
  }
  context = context || window;
  const fn = Symbol();
  context[fn] = this;
  const args = [...arguments].slice(1);
  const result = context[fn](...args);
  delete context[fn];
  return result;
}

2.3 apply

apply 的几种用法:

  1. 同 call;
  2. 链接构造器:
    //创建一个全局Function 对象的construct方法 ,来使你能够在构造器中使用一个类数组对象而非参数列表。
    Function.prototype.construct = function (aArgs) {
      var oNew = Object.create(this.prototype);
      this.apply(oNew, aArgs);
      return oNew;
    };
    
    function MyConstructor (arguments) {
        for (var nProp = 0; nProp < arguments.length; nProp++) {
            this["property" + nProp] = arguments[nProp];
        }
    }
    
    var myArray = [4, "Hello world!", false];
    var myInstance = new MyConstructor(myArray); //Fix MyConstructor.construct is not a function
    
    console.log(myInstance.property1);                // logs "Hello world!"
    console.log(myInstance instanceof MyConstructor); // logs "true"
    console.log(myInstance.constructor);              // logs "MyConstructor"
    

apply 实现

Function.prototype.myApply = function (context) {
    // this 指向调用 mycall 的函数对象,调用 mycall 的若不是函数则报错
    if (typeof this !== 'function') {
        throw new TypeError('Not Function')
    }
    if (!(arguments[1] instanceof Array)) {
        throw new TypeError('arguments[1] Need To Array')
    }
    
    var context = context || window
    context.fn = this
    
    var result
    // 需要判断是否存在第二个参数,如果存在,将其展开
    if (arguments[1]) {
    result = context.fn(...arguments[1])
    } else {
    result = context.fn()
    }
    
    delete context.fn
    return result
}

严格模式下,函数的this值就是call和apply的第一个参数thisArg;非严格模式下,thisArg值被指定为 null 或 undefined 时this值会自动替换为指向全局对象,原始值则会被自动包装,也就是new Object()。

3. 数学运算符

在对各种非Number类型运用数学运算符(- * /)时,会先将非Number类型转换为Number类型;

1 - true // 0
1 - null //  1
1 * undefined //  NaN
2 * ['5'] //  10

注意+是个例外,执行+操作符时:

  1. 当一侧为String类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。
  2. 当一侧为Number类型,另一侧为原始类型,则将原始类型转换为Number类型。
  3. 当一侧为Number类型,另一侧为引用类型,将引用类型和Number类型转换成字符串后拼接。
123 + '123' // 123123   (规则1)
123 + null  // 123    (规则2)
123 + true // 124    (规则2)
123 + {}  // 123[object Object]    (规则3)

4. 类型判断

Object.prototype.toString.call()

准确类型判断:

Object.prototype.toString.call(1)           // "[object Number]"
Object.prototype.toString.call('hi')        // "[object String]"
Object.prototype.toString.call({a:'hi'})    // "[object Object]"
Object.prototype.toString.call([1,'a'])     // "[object Array]"
Object.prototype.toString.call(true)        // "[object Boolean]"
Object.prototype.toString.call(() => {})    // "[object Function]"
Object.prototype.toString.call(null)        // "[object Null]"
Object.prototype.toString.call(undefined)   // "[object Undefined]"
Object.prototype.toString.call(Symbol(1))   // "[object Symbol]"

typeof

typeof 可以判断的类型有如下7种,原理是判断机器码的1~3位:

  1. undefined
  2. number
  3. string
  4. boolean
  5. symbol
  6. object
  7. function

但是在判断一个 object 数据的时候只能告诉我们这个数据是 object, 而不能细致的具体到是哪一种 object:

typeof String(1) //'string'

let s = new String('abc'); //// 除 Function 外的所有构造函数的类型都是 'object'
typeof s === 'object'// true

s instanceof String // true
s instanceof Object // true

// 括号有无将决定表达式的类型。
var iData = 99;
typeof iData + ' Wisen'; // 'number Wisen'
typeof (iData + ' Wisen'); // 'string'

因此在用 typeof 来判断变量类型的时候,我们需要注意,最好是用 typeof 来判断基本数据类型(包括symbol),避免对 null 的判断(typeof null === 'object';)。

Symbol:ES6 新引入的一种原始类型

  • 每个从Symbol()返回的symbol值都是唯一的
  • 如果我们想创造两个相等的Symbol变量,可以使用Symbol.for(key)
  • 作为对象的key时,不可枚举
var obj = {
  name:'ConardLi',
  [Symbol('name2')]:'code秘密花园'
}
Object.getOwnPropertyNames(obj); // ["name"]
Object.keys(obj); // ["name"]
for (var i in obj) {
   console.log(i); // name
}
Object.getOwnPropertySymbols(obj) // [Symbol(name)]

instanceof

用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var auto = new Car('Honda', 'Accord', 1998);

console.log(auto instanceof Car); // true
console.log(auto instanceof Object); // true

jquery中的类型判断:

var class2type = {};
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
function( i, name ) {
	class2type[ "[object " + name + "]" ] = name.toLowerCase();
} );

type: function( obj ) {
	if ( obj == null ) {
		return obj + "";
	}
	return typeof obj === "object" || typeof obj === "function" ?
		class2type[Object.prototype.toString.call(obj) ] || "object" :
		typeof obj;
}

isFunction: function( obj ) {
		return jQuery.type(obj) === "function";
}

原始类型直接使用 typeof,引用类型使用 Object.prototype.toString.call 取得类型,借助一个 class2typ e对象将字符串多余的代码过滤掉,例如 [object function] 将得到 array ,然后在后面的类型判断,如 isFunction 直接可以使用 jQuery.type(obj) === "function" 这样的判断。

判断数组类型

  • Array.isArray()--ES5新增,IE9+
  • Object.prototype.toString.call(value) == "[object Array]"

判断对象类型

  • Object.prototype.isPrototypeOf():用于测试一个对象是否存在于另一个对象的原型链上。
  • Object.getPrototypeOf():ES5新增,返回指定对象的原型(即,内部[[Prototype]]属性的值)。

5. ==类型转换

  • NaN 和其他任何类型比较永远返回false(包括和他自己);
  • null 和 undefined 相等(它们也与其自身相等),除此之外其他值都不和它们两个相等;
  • Boolean 和其他任何类型比较,Boolean 首先被转换为 Number 类型;
  • String 和 Number 比较,先将 String 转换为 Number 类型;
  • 当原始类型和引用类型做比较时,对象类型会依照ToPrimitive规则转换为原始类型;

ToPrimitive原则

一般会调用引用类型的 valueOf 和 toString 方法,你也可以直接重写 toPrimitive 方法。一般转换成不同类型的值遵循的原则不同,例如:

  • 引用类型转换为Number类型,先调用valueOf,再调用toString
  • 引用类型转换为String类型,先调用toString,再调用valueOf
  • 若valueOf和toString都不存在,或者没有返回基本类型,则抛出TypeError异常。
  • 这两个方法一般是交由js去隐式调用,以满足不同的运算情况。

根据 ToPrimitive 抽象操作规则,a + '' 会对 a 调用 valueOf() 方法,然后通过 toString 抽象操作将返回值转换为字符串。而String(a)则是直接调用 toString()。

var a = {
    valueOf: function(){
        return 42;
    },
    toString: function(){
        return 4;
    }
}

a + ""      //42
String(a)   //4

在有运算操作符的情况下,valueOf的优先级高于toString

  • 在数值运算里,会优先调用valueOf(),如 a * b;
  • 在字符串运算里,会优先调用toString(),如alert(c).

可以看一个例子:

var a = { 
  i: 1, 
  valueOf: function () { 
    alert("你调用了a的valueOf函数"); 
    return this.i; 
  }, 
  toString: function () { 
    alert("你调用了a的toString函数"); 
    return this.i; 
  } 
}; 
var c = { 
  i: +a, 
  valueOf: function () { 
    alert("你调用了c的valueOf函数"); 
    return this.i; 
  }, 
  toString: function () { 
    alert("你调用了c的toString函数"); 
    return this.i; 
  } 
}; 
alert(c);

把上面的例子改为:

var c = { 
  i: ''+a, 
  valueOf: function () { 
    alert("你调用了c的valueOf函数"); 
    return this.i; 
  }, 
  toString: function () { 
    alert("你调用了c的toString函数"); 
    return this.i; 
  } 
}; 
alert(c);

还是调用了a的valueOf函数。 把上面的例子改为:

var c = { 
  i: a+a, 
  valueOf: function () { 
    alert("你调用了c的valueOf函数"); 
    return this.i; 
  }, 
  toString: function () { 
    alert("你调用了c的toString函数"); 
    return this.i; 
  } 
}; 
alert(c);

仍然是调用了a的valueOf函数,详细解释见“+运算符”

// 拥有 Symbol.toPrimitive 属性的对象
let obj = {
  [Symbol.toPrimitive](hint) {
    if(hint === 'number'){
      console.log('Number场景');
      return 123;
    }
    if(hint === 'string'){
      console.log('String场景');
      return 'str';
    }
    if(hint === 'default'){
      console.log('Default 场景');
      return 'default';
    }
  }
}
console.log(2*obj); // Number场景 246
console.log(3+obj); // String场景 3default
console.log(obj + "");  // Default场景 default
console.log(String(obj)); //String场景 str

一般情况下,+连接运算符传入的参数是default,而对于乘法等算数运算符传入的是number。对于String(str),${str}等情况,传入的参数是defalut。

Symbol.toPrimitive 和 toString、valueOf

当然,你也可以重写一个不做参数判断的Symbol.toPrimitive方法,结合上面提到的toString,可以有以下例子。

let ab = {
    valueOf() {
        return 0;
    },
    toString() {
        return '1';
    },
    [Symbol.toPrimitive]() {
        return 2;
    }
}
console.log(1+ab);
console.log('1'+ab);

可以看到,Symbol.toPrimitive方法在转换基本类型的时候优先级最高。

6. 原型与原型链

6.1 prototype

每个函数都有一个 prototype 属性,这个属性是一个指针,指向原型对象

  • Object.prototype: 指向 Object 的原型对象。 It is also the end of a prototype chain.
  • Function.prototype: 指向 Function 的原型对象。

使用原型对象的好处是可以让所有对象实例共享它包含的属性和方法。

6.2 constructor

每个对象都有一个 constructor 属性,指向该对象的构造函数

如果将 F.prototype 设置为一个新的字面量对象,constructor属性将不再指向F(会指向Object)

两种维护construcor的方式:

  1. //这种方式constructor会由原来的不可枚举,变成可枚举的
    F.prototype.constructor = F
    
  2. ES5以上版本
    Object.defineProperty(F.prototype, "constructor", {
        enumerable: false,
        value: F
    })
    

6.3 原型对象

每创建一个函数,都会自动为该函数生成一个prototype属性,这个属性指向函数的原型对象。

默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性指向prototype所在的函数(即 F.prototype.constructor === F)。

6.4 [[prototype]]

当调用构造函数创建新实例后,该实例内部将包含一个指针[[prototype]](内部属性),指向构造函数的原型对象。Firefox、Safari、Chrome 将其实现为__proto__,其他环境是不可见的。

注意:这个连接是存在于实例与构造函数的原型对象之间,而实例与构造函数之间。

总结:

  • __proto__和constructor属性是对象所独有的,每个对象都有;
  • prototype属性是函数所独有的,每个函数都有(除了 Function.prototype.bind() 生成的函数);
  • 因为函数也是一种对象,所以函数也拥有__proto__和constructor属性。

6.4 构造函数、原型、实例的关系

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

确定原型与实例的关系

  • instanceof:用于检测构造函数的 prototype (即原型对象)是否出现在实例的原型链上。
  • isPrototypeOf():用于测试一个对象是否存在实例的原型链上。

isPrototypeOf() 与 instanceof 运算符不同。在表达式 "object instanceof AFunction"中,object 的原型链是针对 AFunction.prototype 进行检查的,而不是针对 AFunction 本身。

需要注意的是,如果表达式 obj instanceof Foo 返回 true,则并不意味着该表达式会永远返回 true,因为 Foo.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 obj 的原型链上,这时原表达式的值就会成为 false。

6.5 原型链

因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过__proto__将对象和原型联系起来组成==原型链==,得以让对象可以访问到不属于自己的属性。

所有对象都可以通过原型链最终找到 Object.prototype ,虽然 Object.prototype 也是一个对象,但是这个对象却不是 Object 创造的,而是引擎自己创建了 Object.prototype 。所以可以这样说,所有实例都是对象,但是对象不一定都是实例

所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针 [[prototype]],指向 Object.prototype。

首先引擎创建了 Object.prototype ,然后创建了 Function.prototype ,并且通过__proto__将两者联系了起来。这里也很好的解释了上面的一个问题,为什么 let fun = Function.prototype.bind() 没有 prototype 属性。因为 Function.prototype 是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype 属性。

7. new 操作符

用法:

function Person(name, age){
	this.name = name;
	this.age = age;
}
const person1 = new Person('Tom', 20)
console.log(person1)  // Person {name: "Tom", age: 20}

new 运算符创建一个用户定义的对象类型的实例,或具有构造函数的内置对象的实例。

new 关键字会进行如下的操作:

  1. 创建一个空对象obj(即{});
  2. 将obj的[[prototype]]属性指向构造函数F的原型(即obj.[[prototype]] = F.prototype)。
  3. 将构造函数F内部的this绑定到新建的对象obj,执行F(也就是跟调用普通函数一样,只是此时函数的this为新创建的对象obj而已,就好像执行obj.F()一样);
  4. 如果构造函数无返回值,或者返回值不是引用类型,则返回新对象;否则返回构造函数的返回值。
function Foo() {}
// 这个函数是 Function 的实例对象
// function 就是一个语法糖
// 内部调用了 new Function(...)

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 } 。

new 实现:

function create() {
	// 创建一个空的对象
	let obj = new Object()
	// 获得构造函数
	let Con = [].shift.call(arguments)
	// 链接到原型
	obj.__proto__ = Con.prototype
	// 绑定 this,执行构造函数
	let result = Con.apply(obj, arguments)
	// 确保 new 出来的是个对象
	return typeof result === 'object' ? result : obj
}

引用链接:

  1. 你真的掌握变量和类型了吗
  2. 深度解析原型中的各个难点