循序渐进学编程10.js进阶

原型和原型链

  • 所有的对象都是通过new 函数创建
function test() {
    return {};
}
console.log(new test());//---得到的是一个Object对象
  • 所有的函数也是对象
    函数都是通过new Function()产生的,Function函数是直接在js引擎启动的时候放到内存中的;
  • 函数中可以有属性
  • 所有对象都是引用类型

原型 prototype

所有函数都有一个属性:prototype,称之为函数原型

理解,所有的函数function test() {} 都可以认为是通过new Function的方式创建的,在new Function的时候,给函数添加了一个叫做prototype的属性;

默认情况下,prototype是一个普通的Object对象
prototype = {

}
默认情况下,prototype中有一个属性,constructor,它也是一个对象,它指向构造函数本身。

Object.prototype.constructor === Object //true

隐式原型 proto

所有的对象都有一个属性:__proto__,他是一个对象,称之为隐式原型

默认情况下,隐式原型指向创建该对象的构造函数的原型

function A() {}
function B() {}
function create () {
    if(Math.random() < 0.5) {
        return new A();
    } else{
        return new B();
    }
}

var obj = create();
// 问:如何得到创建obj的构造函数名称?
// 答:obj.__proto__.constructor.name

当访问一个对象的成员时:

  1. 看该对象自身是否拥有该成员,如果有直接使用
  2. 看该对象的隐式原型对象中是否该成员,如果有则直接使用
  3. 在原型链中依次查找

猴子补丁:在函数原型中加入成员,以增强对象的功能,猴子补丁会导致原型污染,使用需谨慎.

原型链

特殊点:

  1. Function的__proto__指向自身的prototype
  2. Object的prototype的__proto__指向null
var arr = [1, 2, 4, 2];
arr.toString();//Array.prototype.toString()覆盖了String.prototype.toString()
String.prototype.toString().call(arr);//[object,Array]---强行使用String 的toString方法

面试题

// 1.
var F = function () {};
Object.prototype.a = function () {};
Function.prototype.b = function () {};

var f = new F();
console.log(f.a, f.b, F.a, F.b);//fn, undefined, fn , fn


// 2.
function A() {};
function B(a) {
    this.a = a;
}
function C(a){
    if(a) {
        this.a = a;
    }
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new A().a);//1
console.log(new B().a);//undefined
console.log(new C(2).a);//2

// 3.
function User() {}
User.prototype.sayHello = function () {}

var u1 = new User();
var u2 = new User();

console.log(u1.sayHello === u2.sayHello);//true
console.log(User.prototype.constructor);//User
console.log(User.prototype === Function.prototype);//false
console.log(User.__proto__ === Function.prototype);//true
console.log(User.__proto__ === Function.__proto__);//true
console.log(u1.__proto__ === u2.__proto__);//true
console.log(u1.__proto__ === User.__proto__);//false
console.log(Function.__proto__ === Object.__proto__);//true
console.log(Function.prototype.__proto__ === Object.prototype.__proto__);//flase
console.log(Function.prototype.__proto__ === Object.prototype);//true;

原型链的应用

基础方法

W3C不推荐直接使用系统成员__proto__

Object.getPrototypeof(对象)

获取对象的隐式原型

Object.prototype.isPrototypeOf(对象)

判断当前对象(this)是否在指定对象的原型链上

Function.prototype.isPrototypeOf(对象)…同理

对象 instanceof 函数

某个对象是不是某个东西

[].instanceof Array — 可以用来判断[]是否为一个真数组,但是如果页面中有iframe元素的话不推荐使用(有ifram元素的话,数组的指向会发生变化)

判断函数的原型是否在对象的原型链上

Object.create(对象)

创建一个新对象,其隐式原型指向指定的对象

// 面试
var obj = {x:1, y:33}
var obj1 = Object.create(obj);//此时obj1的隐式原型中没有constructor

// 关于所有对象原型链上最终是否是Object.prototype
// 一般来说,对象的隐式原型都指向Object.prototype
var obj2 = Object.create(null);//此时obj2的隐式原型指向null

Object.prototype.hasOwnProperty(属性名)

判断一个对象自身是否拥有某个属性

var p = {
    x:123,
    y:456
};
var obj = Object.create(p);

for(var prop in obj) {
    console.log(prop);//在循环对象属性的时候,原型链上的东西也会
    //被循环,如果只想得到obj对象自身的属性,则加上判断即可 
    //if(obj.hasOwnProperty(prop)){ console.log(prop) };
}

应用

类数组转换为真数组

Array.prototype.slice.call(类数组);

实现继承

默认情况下,所有构造函数的父类都是Object

圣杯模式:实现继承的模式

function User(firstName, lastName, age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.fullName = this.firstName + '' + this.lastName;
}

User.prototype.sayHello = function () {
    console.log(`大家好,我叫${this.fullName}, 今年${this.age}岁了`);
}

function VIPUser(firstName, lastName, age, money){
    User.call(this, firstName, lastName, age);
    this.money = money;
}
VIPUser.prototype.upgrade = function () {
    console.log(`恭喜您消费${100}元,提升了1个等级!`);
}


// 继承 标准的圣杯模式  面试

if(!this.myPlugin) {
    this.myPlugin = {};
}

this.myPlugin.inherit = function (Son, Father) {
    Son.prototype = Object.create(Father.prototype);//es5
    //es5之前的写法
    //var Temp = function () {};
    //Temp.prototype = Father.prototype;
    //son.prototype = new Temp();

    Son.prototype.uber = Father.prototype;//圣杯模式
    // 这里的uber在以前是super,但是super已经作为一个保留字,
    //含义是父类的意思,子类的原型有一个属性指向父类的原型,
    //可以用来获取子类原型的隐式原型指向;es5之后可以用
    //Object.hasPrototypeOf(obj),来获取obj的隐式原型,也可以用
    //Son.prototype.__proto__来获取,但是__proto__是系统属性,w3c不推荐直接使用系统属性
    // 这里更为优略的一种写法是
    Son.prototype.uber = Father;
    //这样的写法好处是在子类中无须调用父类,直接调用Son.uber即可
    Son.prototype.constrcutor = Son;
}
//YaHu公司高效继承写法---调用多次也不会出问题
this.myPlugin.inherit = (function () {
    var Temp = function () {}
    return function (Son, Father) {
        Temp.prototype = Father.prototype;
        Son.prototype = new Temp();
        Son.prototype.uber = Father;
        Son.prototype.constructor = Son;
    }
}())

属性描述符

参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

属性描述符:它表达了一个属性的相关信息(元数据),它本质上是一个对象.
元数据:描述数据的数据

  1. 数据属性:通过直接赋值或者在初始化的时候添加的属性都是数据属性;
  2. 存取器属性
    1. 当给他赋值时,会自动运行一个函数
    2. 当获取它的值时,会自动运行一个函数
  • 静态方法
Object.defineProperty(对象, 属性名-字符串, {
    属性描述符
    // value:xxx,
    get:function () {
        //当读取属性x时,运行的函数
        console.log('读取属性x');
        //该函数的返回值,将作为属性值
        return 2;
    },
    set:function (val) {
        //当给该属性赋值时,运行的函数
        //val:表示要赋的值
        console.log('给属性赋值为' + val);
    }
})

obj.x = 3;//相当于运行set(3);

一个表达式的返回值是赋给它的值

dom的innerText、background属性是一个存取器属性,页面进行了重排

其他的属性描述符

  • configurable:默认为false,表示属性描述符是否能被后续修改,设置为true之后,不仅可以被修改,也可以被delete
  • writeable:在存取描述符中无效,表示是否可重写
  • enumerable:是否可被遍历(for in 循环),默认为false

Object.getOwnPropertyDescriptor(对象, ‘属性名’)

获取某个对象的某个属性的属性描述符对象(该属性必须直接属于该对象)

执行上下文

执行上下文:一个函数运行之前,创建的一块内存空间,空间中会包含该函数执行所需要的数据,为该函数执行提供支持。

执行上下文栈 : call back,所有执行上下文组成的内存空间;
栈:先进后出的数据结构

预编译:
函数预编译:

  1. 生成AO对象—函数执行上下文
  2. 找到函数的形参与变量声明,作为AO对象的属性,值为undefined
  3. 形参实参相结合
  4. 找到函数声明,作为AO对象的属性,值为函数体

全局预编译

  1. 生成GO对象—全局执行上下文
  2. 找到变量声明,作为GO对象的属性,值为undefined
  3. 找到函数声明,作为GO对象的属性,值为函数体

全局执行上下文:所有js代码执行之前,必须拥有该环境
函数执行上下文:函数调用之前创建

js引擎始终指向的是栈顶的上下文

执行上下文中的内容

  1. this指向
    1). 直接调用函数,this指向全局对象
    2). 在函数外,this指向全局对象
    3). 通过对象调用或new一个函数,this指向对象或一个新对象
  2. VO 变量对象
    variable Object:VO 记录了该环境中所有声明的参数、变量和函数
    全局的VO指向的是window,全局的this也指向window

Global Object:GO 全局执行上下文中的VO

Active Object:AO 当前正在执行的上下文中的VO

  1. 确定形参以及特殊变量arguments
  2. 确定函数中通过var声明的变量,把它们的值设置为undefined,如果VO中已有该名称,则将变量声明直接忽略—如果是实参是个对象的话,实参指向的是外部的引用
  3. 确定函数中通过字面量声明的函数,将他们的值设置为指向函数对象,如果VO中已存在该名称,则覆盖

当一个上下文中的代码执行的时候,如果上下文中不存在某个属性,则会从之前的上下文中寻找.

var a = 1;
function b () {
    console.log(a);
    a = 10;
    return;
    function a(){ }
}
b();
console.log(a);
//function a() {}, 1

作用域链

  1. VO中包含一个额外的属性,该属性指向创建该VO的函数本身(全局上下文除外),如果是全局中的函数,则这个函数的额外属性指向全局上下文
  2. 每个函数在创建时,会有一个隐藏属性[[scope]],这个属性无法访问,它指向创建该函数时的AO(正在 执行的上下文)
  3. 当访问一个变量时,会先查找自身VO中是否存在,则依次查找[[scope]]属性;---->闭包
function A() {
    var count = 0;
    return function () {
        count++;
        console.log(count);
    }
}
var test = A();
test();//1
test();//2
test();//3
console.log(count);//报错
// 闭包从广义上来讲就是函数里面可以用函数外面的变量(JavaScript中每个函数在
//创建的时候都有一个属性[[scope]]指向在创建这个函数时(函数声明的时候)正在
//执行的上下文,所以函数都携带了一个周围上下文的引用,这样的组合就是闭包。
//狭义上来讲,函数内部的东西被保存到外部,本来函数执行完之后,AO销毁,无法
//在找到那些函数内部的变量,但是当内部的变量通过函数被保存到外部之后,因为
//每一个函数创建的时候都会有一个[[scope]]指向在创建自己时的AO,所有AO并不
//会消失,所以可以通过外部变量访问函数内部的变量,除非将保存有函数内部变量
//的外部变量赋值为null)


// 
var a = 1;
function A() {
    console.log(a);
}

function Special(){
    var a = 5;
    var B = A;
    B();
}
Sepcial();//1;
// 闭包是在创建函数的时候形成的,而不是运行函数的时候

某些浏览器会优化作用域链,函数的[[scope]]仅保留需要用到的数据

//1.
var foo = {n:1};
(function (foo){
    console.log(foo.n);
    foo.n = 3;
    var foo = {n:2};
    console.log(foo.n);
}(foo))
console.log(foo.n);
//1 2 3

//2.
var food = 'rice';
var eat = function () {
    console.log(`eat ${food}`);
};
(function (){
    var food = 'noddles';
    eat();
}())
// eat rice
// 函数的[[scope]]始终指向的是该函数在创建的时候的上下文;
//3.
var food = 'rice';
(function (){
    var food = 'noddles';
    var eat = function () {
        console.log(`eat ${food}`);
    };
    eat();//eat noddles
}())

//4.闭包

function A(){
    for(i = 0; i < 10;i++) {
        setTimeout(function() {
            console.log(i)
        }, 1000);
    }
}
A();//10 * 10
console.log(i);//10

function A(){
    for(i = 0;i < 10;++i) {
        (function(i){
            console.log(i);
        }(i))
    }
}
A();//1 2 3 4 5 6 7 8 9 10

事件循环

异步:某些函数不会立即执行,需要等到某个时机成熟后才会执行,该函数叫做异步函数

异步可以很大的提高系统吞吐量;

浏览器的线程:多线程的

  1. js执行引擎:负责执行JS代码—js是单线程的
  2. 渲染线程:负责渲染页面
  3. 计时器线程:负责计时—setTimeout函数的计时是靠这个方法的;
  4. 事件监听线程:负责监听事件
  5. http网络线程:负责网络通信

事件队列:是一块内存空间,用于存放执行时机达到的异步函数—特点:先进先出。当js引擎空闲(执行栈没有可执行的上下文),他会从事件队列中拿出第一个函数执行

document.querySelector('button').onclick = function A() {
    setTimeout(function B() {
        console.log('异步代码');
    },0);
    loop();
}
function loop() {
    for(var i = 0;i < 1000; ++i) {
        console.log(i);
    }
};
loop();
//异步函数需要在队列中排队,call stack不为空的时候不可能执行异步函数

js中,某个函数的执行不可能被打断,因为js执行引擎是单线程的;

事件循环:event loop,是指函数在执行栈、宿主线程、事件队列中的循环移动

对象混合和克隆

对象混合

obj2混合到obj1产生新对象
两个对象,混合后产生一个新对象,obj2的属性全部保留到新对象,obj1中有但是obj2中没有的属性添加到新对象
混合:mixin

/*将obj2混合到obj1中*/
this.myPlugin.minxin = function(obj1, obj2) {
    var newObj = {};
    for(var prop in obj2) {
        newObj[prop] = obj2[prop];
        //当属性是一个变量的时候,不能使用.访问属性
    }
    for(var prop in obj1) {
        if(!(prop in obj1)) {
            newObj[prop] = obj1[prop];
        }
    }
    return newObj;
} 

静态方法:
Object.assign(obj1, obj2);将obj2混合到obj1,这里的本质是obj2复制到obj1,所以obj1也发生了改变,返回的obj和obj1是相等的

解决办法:Object.assign({}, obj1, obj2);

function complicate(option) {
    var defaultObj = {
        a:"default-a",
        b:"default-b",
        c:"default-c",
        d:"default-d",
        e:"default-e"
    };
    return Object
}
 

克隆

  • 没有封装好的方法用来克隆
//浅度克隆,属性直接复制;如果属性是一个对象,
//那么这两个对象的地址相同,改变其中任何一个,都会导致两个都改变
function clone(obj, deep) {
    if (Array.isArray(obj)) {
        var newArr = [];
        if (deep) {
            for (var i = 0; i < obj.length; ++i) {
                newArr.push(clone(obj[i], deep));
            }
        } else {
            return newArr.slice(obj);
        }
        return newArr;
    } else if (typeof (obj) === 'Object') {
        var newObj = {};
        if (deep) {
            for (var prop in obj) {
                newObj[prop] = clone(obj[prop], deep);
            }
        } else {
            for (var prop in obj) {
                newObj[prop] = obj[prop];
            }
        }
    } else {
        //函数、原始类型
        return obj;
    }
    return newObj;
}

递归是函数式编程的语法,函数式编程的特点:不考虑程序执行具体细节,只在逻辑层面描述程序

声明式编程:函数式编程、html、css
命令式编程:c++,javascript,c#,js

函数防抖和函数节流

函数防抖 debounce

类似于电梯红外感应,每次感应就重新等3s,之后把电梯门关上、百度搜索框感知,每次按键后监听是否按键完成、resize事件…


回调函数:通常将作为函数的参数来调用的函数叫做回调函数
高阶函数:在函数内部返回一个新的函数-可能会产生闭包

myPlugin.debounce = function (callback, duration) {
    var timer;
    return function () {
        clearTimeout(timer);
        var args = arguments;
        timer = setTimeout(function () {
            callback.apply(null, args)
        }, duration)
    }

}


var debounce = myPlugin.debounce(function(val, inp) {
    console.log(val);
    console.log('这里的this是' + inp);
}, 1000);
var inp = document.createElement('input');
document.body.appendChild(inp);
inp.oninput = function () {
    debounce(this.value, this);
}
// 说明:这里用高阶函数的原因是不想产生全局变量,如果这里直接写,那么每调用一次防抖函数的时候timer都是undefined,所以产生的定时器都是新的,我们需要的整个防抖函数只是一个timer,这里利用闭包的特性,局部变量被保存到外部不能清除函数的VO,所以timer被保村了下来

函数节流 throttle

保证一个时间段内执行一次

// 函数节流有两种写法
//1. 每隔time s后触发一次---用计时器
myPlugin.throttle = function (callback, time) {
    var timer;
    return function () {
        if(timer) {
            return;
        }
        var args = arguments;
        timer = setTimeout(function () {
            callback.apply(null, args);
            timer = null;
        }, time)
    }
}
//2. 第一次马上触发,下一次等待time s后触发---用时间戳

myPlugin.throttle = function (callback, time) {
    var t;
    return function () {
        if(!t || Date.now() - t >= time) {
            callback.apply(null, arguments);
        }
    }
}

//3. 使用第三个参数来确定是选第一种写法还是第二种写法

myPlugin.throttle = function (callback, time, immediate) {
    var t, timer;
    if(immediate) {
        immediate = true;
    }
    if(immdiate) {
        return function () {
            if(!t || Date.now() - t >= time) {
                callback.apply(null, arguments);
            }
        }
    } else{
        return function () {
        if(timer) {
            return;
        }
        var args = arguments;
        timer = setTimeout(function () {
            callback.apply(null, args);
            timer = null;
        }, time)
    }
    }
}

函数防抖和函数节流的区别:
函数节流:如果条件触发频繁,固定多少秒运行一次
函数防抖:如果条件触发频繁,当触发结束之后只运行一次

柯里化

curry:函数式编程里面的东西, 柯里化就是一个函数
柯里化函数:固定某个函数的一些参数,得到该函数剩余参数的一个新函数,如果没有剩余参数,则调用

所有函数都一个属性length,表示它接受几个形参

有些函数写的过于通用,所以当多次使用它的时候,如果其他一些参数值是固定的,就可以使用柯里化

在函数式编程中,柯里化最重要的作用就是把多参函数转为单参函数

myPlugin.curry = function (func) {
    var args = Array.prototype.slice.apply(arguments, 1);
    var that = this;
    return function () {
        var remainderArgus = Array.from(arguments);
        var totalArgus = args.concat(remainderArgus);
        if(func.length === totalArgus.length) {
            return func.apply(null, totalArgus);
        } else{
            totalArgus.unshift(func);
            return that.apply(null, totalArgus);
        }
    }
}

函数管道

函数管道:将多个单参函数组合起来,形成一个新的函数,这些函数前一个函数的输出是后一个函数的输入(好比多级放大电路)

比如要写smallCamel函数, 已经有三个写好的单参函数,然后利用函数管道

某些多参函数也需要使用函数管道的话,可以将多参函数使用柯里化变成单参函数

myPlugin.pipe = function () {
    var args = Array.from(arguments);
    return function (val) {
        for(var i = 0; i < args.length;++i) {
            val = args[i](val);
        }
        return val;
    }
    /*
    //另外一种写法
    var args = Array.from(arguments);
    return function (val) {
        args.reduce(function (result, cur) {
            return cur(result);
        }, val)
    } 
   
    */
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值