JavaScript自检清单

一. 自省方法,等值判断,数据类型特性

let a = '123'
let b = [1,2]
const c = {a:'123', b:'456'}
function A(){}
A.prototype.name = 'A'
const a1 = new A

// 基础类型判断
typeof a 					// string
a instanceof String 		// true

// 对象/数组类型判断
b instanceof Array			// true
Array.isArray(b)			// true

// NaN值判断
Number.isNaN(NaN)			// true
Object.is(NaN, NaN)			// true

// 属性可枚举
c.propertyIsEnumerable('a') 			 // true
Object.propertyIsEnumerable.call(c, 'a') // true

// 是否是自有属性
c.hasOwnProperty('a')				// true
Object.hasOwnProperty.call(c, 'a')	// true
a1.hasOwnProperty('name')			// false (name属性在原型对象而非实例上)
A.prototype.hasOwnProperty('name')	// true

// 判断对象或对象的原型链上是否有该属性
'name' in a1 // true
'name' in A.prototype // true

// 对象原型上是否有该属性
hasPrototypeProperty(a1, 'name') // true
a1.name = 'cc'
hasPrototypeProperty(a1, 'name') // false (被覆盖)

// 实例与原型对象关系确定
a1.__proto__.constructor = A // true
A.prototype.isPrototypeOf(a1) // true
a1 instanceof A // true

// 获取实例的原型对象
a1.__proto__ == A.prototype
Object.getPrototypeOf(a1) == A.prototype // true

// 创建新对象并指定原型
let a2 = Object.create(A.prototype)

// 获取属性
for...in		// 拿到实例和原型上的属性
Object.keys // 仅拿到实例上的属性(不包括不可枚举属性)
Object.getOwnPropertyNames	// 仅拿到实例属性(包括不可枚举属性)
Object.getOwnPropertySymbols // 拿到实例属性中的符号属性(Symbol)

二. 原型、对象、继承

1. 对象的属性

对象属性分两种:数据属性和访问器属性
数据属性:

  • [[Configurable]]  是否可删除/特性是否可修改
  • [[Enumberable]]  是否可迭代
  • [[Writable]]  值是否可修改
  • [[Value]]  属性实际值,默认为 undeifined

访问器属性:

  • [[Configurable]]  是否可删除/特性是否可修改
  • [[Enumberable]]   是否可迭代
  • [[Get]]
  • [[Set]] 

2. new操作符

  =
两张图,一个是工厂模式,在函数内部显示创建对象
另一个是构造函数,通过 new 操作符来创建对象
按照红宝书里的解释, new  操作符实现了下面步骤:
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即this指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
按照步骤实现,即:

let o = new Object()
o.__proto__ = A.prototype
// 3,4 步并做一步
A.call(o)
a = o;

3. 实例、构造函数、原型对象

function A(){}
let a = new A
let b = new A
  • 分别是什么?在上面的例子中,实例是 a,原型对象是 A.prototype,构造函数是 A
  • 联系是什么?
// 下面等式结果均为 true
A.prototype.constructor === A
A.prototype.__proto__ === Object.prototype

// 正常的原型链都会终止于Object的原型对象
A.prototype.__proto__.constructor === Object
// Object原型的原型是null
A.prototype.__proto__.__proto__ === null

// 实例通过__proto__链接到原型对象
a.__proto__ === A.prototype
// 构造函数通过prototype属性链接到原型对象
a.__proto__.constructor === A

// 同一个构造函数创建的两个实例,共享同一个原型对象:
a.__proto__ === b.__proto__

2,3行可以看出原型 A.prototype 的构造函数是 A,再往上找是 Object的原型
6,8行 原型 A.prototype 的原型链上的构造函数是 Object, 原型Object的原型链往上就是 null
11,13行,再看实例和原型的关系 实例a的原型链指向 A的原型,实例a原型链上的构造函数指向 A
通过这么一番梳理,我自己是理解了,关键是搞清楚实例,构造函数,原型对象分别是什么,这样才能梳理逻辑。

4. 对象的行为

(1) 在对象实例中找一个属性,找不到则会去对象原型上去找
(2) 实例和对象原型拥有同一个属性时,实例属性会覆盖对象原型,且实例属性的修改和原型属性无关;覆盖后,设置实例上的同名属性为null,也不会从对象原型去找,可以通过 delete 该实例属性以使属性访问与对象原型建立联系
(3) 枚举顺序,大部分方法根据浏览器而定,但有些方法是固定的Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.assign()

  • 升序枚举数值键
  • 以插入顺序枚举字符串和符号键

(4) 实例生成之后去向原型对象添加属性和方法,这些属性和方法在实例上也会体现。但是直接替换原型对象则不会反映到实例上(即使指定 constructor 为原构造函数也不行)。简单来说就是不要直接给原型对象赋值。
(5) 原型对象的属性在实例间共享,如果属性值为引用类型(比如 Array ),在实例1中push一个值,在实例2中该属性值也会改变

5. 对象创建方式

这个真的是绕不过去的坎,每本书都有,还总记不住:

  1. 工厂模式:

优点是接口封装,可以批量产生不同的对象
缺点是总是要在函数内显示的创建对象且不知道创建的对象类型

function a(name){
	let o = new Object()
  o.name = name
  return o
}
let b = a('bob') // {name: 'bob'}
  1. 构造函数

缺点是内部定义的方法在实例化时总被定义一遍,实际上是同一个方法,若将方法定义到函数外会影响全局对象。

function A(name){
	this.name = name;
  this.say(){
  	return 'hi'
  }
}
let a = new A('bob')
let b = new A('join')
a.say() // hi
b.say() // hi
  1. 原型模式

通过原型方法共享解决了2的缺点,但如果属性有引用类型,会修改原型

function A(){}
A.prototype.name = '123'
A.prototype.f = [1,2,3]
let a = new A
let b = new A
a.f.push(4)
b.f // [1,2,3,4]
  1. 继承

原型链继承也有3中的缺点,而且b实例初始化时不能向A.prototype传参

function A(name){
	this.name = name;
}
function B(){}
B.prototype = new A
let b = new B
  1. 经典继承/对象伪装

解决了3中修改原型的问题

function A(name){
	this.name = name;	
}
function B(){
	A.call(this, 'Bob')
}
B.prototype = new A
let b = new B
b.name // Bob
  1. 组合继承

解决了3,4的问题, 所以它是目前使用最多的继承模式。缺点是每次实例化父原型会被调用两次

function A(name){
	this.name = name;
  this.colors = [1,2,3]
}
function B(name){
	A.call(this, name)
}
B.prototype = new A // 第一次
let a = new B
let b = new B('Bob') // 第二次
a.colors.push(4)
a.colors // [1,2,3,4]
b.colors // [1,2,3]
b.name // Bob
  1. 原型式继承

存在3中引用类型共享的问题,适用场景是需要对象间共享信息且不需要单独创建构造函数的情况,跟 Object.create 只有一个参数时的行为一样。

function object(o){
	function F(){}
  F.prototype = o;
  return new F
}
let o = {name: '11', friend:[1,2,3]}
let a1 = object(o)
a1.name = 'a1'
a1.friend.push(4)
let a2 = object(o)
a2.name = 'a2'
a2.friend.push(5)
o.friend // [1,2,3,4,5]
  1. 寄生式继承

适用场景是对象信息共享,类似工厂模式+原型式继承,给传来的对象属性增强,同样存在原型式继承的缺点,还有构造函数中的函数重用问题。

function a(o){
  // 调用7中的方法
	let clone = object(o)
  clone.say = function(){
  	console.log('hi')
  }
}
let a3 = a(o)
a3.name = 'a3'
a3.say() // hi
  1. 寄生式组合继承

解决组合继承中每次实例化父类会被调用两次的问题,目前是引用类型继承最佳实践。

function compose(sub, sup){
	let pro = object(sup)	// 创建对象
  pro.constructor = sub // 增强对象
  sub.prototype = pro		// 赋值对象
}

第2行代码创建了父类的副本,然后3、4两行代码将子类的原型指向父类副本。
最后再看看寄生式组合继承如何使用

//声明了构造函数
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
//为构造函数添加原型方法,以使实例能共享方法
SuperType.prototype.sayName = function() {
  console.log(this.name);
};
// 组合继承,解决引用类型属性共享问题,且能传参
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
// 寄生式组合继承,解决了父类两次调用问题
compose(SubType, SuperType);

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

let b1 = new SubType('Bob', 12)
let b2 = new SubType('John', 13)
b1.colors.push('pink')
b1.colors // ["red", "blue", "green", "pink"]
b2.colors // ["red", "blue", "green"]

三. 类、类继承、生成器/迭代器、代理/反射

1. 类的行为

(1) 类定义不能提升,函数声明可以
(2) 类受块作用域限制,函数受函数作用域限制
(3) 类表达式名称可选,当类名与变量名同时存在时,外部访问只能通过赋值的变量名访问。
(4) 使用new操作符实例化类和实例化函数的步骤相同。当类中声明 constructor 函数时,实例化使用的是内部 constructor 函数
(5) 不用new操作符调用类会报错
(6) 类中构造函数返回值非this时,使用 instanceof 不会检测出实例与类有关联,因为对象的原型指针没有修改
(7) 类是JS中的一等公民,可以被当作参数传递
(8) 类的继承中,super方法得到的是父类实例,只能在constructor或静态方法中使用。在constructor中使用时,必须在this添加属性之前,否则获取不到 this 会抛出错误。

2. 类是原型的语法糖?

代码地址->
利用Babel解析工具来看看类的本质,我们先创建个简单的类 A,拥有如下方法

解析后的代码有些长,把它分成三块来分析:
首先是一个工具方法· _createClass

'use strict'; //严格模式下操作

var _createClass = function () {
  	// 这个方法等同于 ES6中的Object.defineProperties,给对象添加多个数据属性
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }
  	/**
    * @param Constructor 构造函数
    * @param protoProps	 原型属性,最终添加到 Constructor.prototype,即原型上
    * @param staticProps 静态属性,最终添加到构造函数上,即 A.xx
    * return Constructor 返回增强后的构造函数
    */
    return function (Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
}();

看完了属性处理方法,再来看看什么情况下会抛错

// 接收参数为实例和构造函数, 判断实例的原型构造函数是否是Constructor
// 上面类的行为第6点,类中构造函数返回值非this时,使用 instanceof 不会检测出实例与类有关联,
// 这个时候就会抛错
function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

声明的工具函数都说完了,接下来到调用

var A = function () {
    function A(name) {
      	// 检测原型
        _classCallCheck(this, A);

        this.name_ = name;
        return this;
    }

  	// 给类添加方法
  	// myName,生成器方法和访问属性被添加到原型对象上,即A.prototype, 方法可以被实例共享
  	// 最后一个 sayName静态方法,被添加到A上,不能被共享,只能通过A.sayName调用
    _createClass(A,
        [{
            key: 'myName',
            value: function myName() {
                console.log(this.name_[0].toUpperCase() + this.name_.slice(1));
            }
        }, {
            key: 'NicknameIterator',
            value: /*#__PURE__*/regeneratorRuntime.mark(function NicknameIterator() {
                return regeneratorRuntime.wrap(function NicknameIterator$(_context) {
                    while (1) {
                        switch (_context.prev = _context.next) {
                            case 0:
                                _context.next = 2;
                                return 'Jack';

                            case 2:
                                _context.next = 4;
                                return 'Jake';

                            case 4:
                                _context.next = 6;
                                return 'J-Dog';

                            case 6:
                            case 'end':
                                return _context.stop();
                        }
                    }
                }, NicknameIterator, this);
            })
        }, {
            key: 'n',
            get: function get() {
                return this.name_;
            },
            set: function set(val) {
                this.name_ = val;
            }
        }],
        [{
            key: 'sayName',
            value: function sayName() {
                console.log('my name is ' + this.name);
            }
        }]);

    return A;
}();

3. 类的继承

写一个继承类,同样查看其原型实现

class A{}
class B extends A {}

Babel转换后

var A = function A() {
    _classCallCheck(this, A);
};
// B继承A
var B = function (_A) {
    _inherits(B, _A);
    function B() {
        _classCallCheck(this, B);
        return _possibleConstructorReturn(this, (B.__proto__ || Object.getPrototypeOf(B)).apply(this, arguments));
    }
    return B;
}(A);

重点放在 _inherits 和 _possibleConstructorReturn 方法做了什么,大概能猜出来

  • _inherits 将类B的__proto__指向类A,复制A类的方法到B,以使AB建立联系
  • _possibleConstructorReturn返回继承到的类
function _possibleConstructorReturn(self, call) {
    if (!self) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
  	// 此例中call即B.__proto__,也就是A 存在且等式成立,返回 A
    return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
    }
  	// 拷贝原型,并赋值给子类的原型
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass, enumerable: false, writable: true, configurable: true
        }
    });
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

4. 新增/模糊/不熟悉的方法

数组
flat 数组平铺
from 类数组对象转为数组,空值占位:

let b=Array.from([,,,])
b // [undefined,undefined,undefined,undefined]

of  将一组参数转为数组
some 数组有一项为true则为true
every 数组每项全true则返回true
includes 数组包含某值
fill() 批量复制方法
copyWithin() 填充数组方法
slice()创建一个包含原有数组中一个或多个元素的新数组, 两个参数为开始到结束,不包括结束。
splice()在数组中间插入元素

  • 删除, 第一个参数为起始位置,第二个参数为删除数量
  • 插入, 第二个参数设置为0,后面任意参数为插入元素值
  • 替换, 第二个参数非0


Date
Date.parse 
getTime() 


字符串
charAt()  方法返回给定索引位置的字符
concat() ,将一个或多个字符串拼接成一个新字符串
slice() 、 substr()  和 substring()  提取字符串

  • 参数区别: substr  第二个参数是提取数量, slice  和 substring  两个参数都是开始位置和结束位置
  • 负数区别:substr 第二个参数为负数时转为0, substring  将负数参数转为0,slice 为长度+负数

indexOf()  和 lastIndexOf() 。这两个方法从字符串中搜索传入的字符串,并返回位置(如果没找到,则返回-1)
startsWith() 、 endsWith()  和 includes()  包含方法
trim  去两边空格
repeat 重复值


对象方法

  • Object.defineProperties  可以同时定义对象上的多个属性
  • Object.getOwnPropertyDescriptor()  可以取得指定属性的属性描述符
  • Object.getOwnPropertyDescriptors()  ES2017新增 相当于对传入对象的每个属性调用Object.getOwnPropertyDescriptor(),返回结果
  • Object.assign()  浅复制、对象合并, 原理是调用待拷贝对象的 get 方法获取到值,再调用结果对象的set方法赋值,最终将待拷贝对象的可枚举的自有属性拷贝下来。
  • Object.is()  接收两个参数判断是否相等,类似于 ===,但对边界值的判断做了标准化处理,不会因浏览器产生差异
console.log(Object.is(true, 1));  // false
console.log(Object.is({}, {}));   // false
console.log(Object.is("2", 2));   // false

// 正确的0、-0、+0相等/不等判定
console.log(Object.is(+0, -0));   // false
console.log(Object.is(+0, 0));    // true
console.log(Object.is(-0, 0));    // false

// 正确的NaN相等判定
console.log(Object.is(NaN, NaN)); // true

5. 生成器/迭代器定义

能够控制暂停和执行并实现 iterator 接口的函数就是生成器,除箭头函数外,表现形式为函数名前加 * 。
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。
可以被迭代的对象为可迭代对象,可迭代对象中都实现了 iterator  接口(可迭代协议),该接口满足两个特点:

  • 实现了 next 方法,返回 iteratorResult  对象,包含 done  和 value  属性
  • 暴露 Symbol,iterator  属性作为默认迭代器

6. 代理、反射

代理的作用:可以拦截对目标对象的属性操作
代理的使用new Proxy(target, handler) , Proxy 是构造函数,接受两个参数, target  目标对象,和 handler  代理行为。
代理的应用场景: 跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。
代理的缺点

  • 目标对象方法中有this值时可能会得到预料外的结果
  • 部分内置类型的方法无法调用,如 Date
const target = new Date();
const proxy = new Proxy(target, {});

console.log(proxy instanceof Date);  // true
proxy.getDate();  // TypeError: 'this' is not a Date object

反射API与 Object 非常相似,主要是在代理的过程中使用,来实现对象的原始操作。

const target = {
  foo: 'bar'
};

const handler = {
  get: Reflect.get
};

const proxy = new Proxy(target, handler);

console.log(proxy.foo);  // bar
console.log(target.foo); // bar

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页