文章目录
原型和原型链
- 所有的对象都是通过
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
当访问一个对象的成员时:
- 看该对象自身是否拥有该成员,如果有直接使用
- 看该对象的隐式原型对象中是否该成员,如果有则直接使用
- 在原型链中依次查找
猴子补丁:在函数原型中加入成员,以增强对象的功能,猴子补丁会导致原型污染,使用需谨慎.
原型链
特殊点:
- Function的__proto__指向自身的prototype
- 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
属性描述符:它表达了一个属性的相关信息(元数据),它本质上是一个对象.
元数据:描述数据的数据
- 数据属性:通过直接赋值或者在初始化的时候添加的属性都是数据属性;
- 存取器属性
- 当给他赋值时,会自动运行一个函数
- 当获取它的值时,会自动运行一个函数
- 静态方法
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,所有执行上下文组成的内存空间;
栈:先进后出的数据结构
预编译:
函数预编译:
- 生成AO对象—函数执行上下文
- 找到函数的形参与变量声明,作为AO对象的属性,值为undefined
- 形参实参相结合
- 找到函数声明,作为AO对象的属性,值为函数体
全局预编译
- 生成GO对象—全局执行上下文
- 找到变量声明,作为GO对象的属性,值为undefined
- 找到函数声明,作为GO对象的属性,值为函数体
全局执行上下文:所有js代码执行之前,必须拥有该环境
函数执行上下文:函数调用之前创建
js引擎始终指向的是栈顶的上下文
执行上下文中的内容
- this指向
1). 直接调用函数,this指向全局对象
2). 在函数外,this指向全局对象
3). 通过对象调用或new一个函数,this指向对象或一个新对象 - VO 变量对象
variable Object:VO 记录了该环境中所有声明的参数、变量和函数
全局的VO指向的是window,全局的this也指向window
Global Object:GO 全局执行上下文中的VO
Active Object:AO 当前正在执行的上下文中的VO
- 确定形参以及特殊变量arguments
- 确定函数中通过var声明的变量,把它们的值设置为undefined,如果VO中已有该名称,则将变量声明直接忽略—如果是实参是个对象的话,实参指向的是外部的引用
- 确定函数中通过字面量声明的函数,将他们的值设置为指向函数对象,如果VO中已存在该名称,则覆盖
当一个上下文中的代码执行的时候,如果上下文中不存在某个属性,则会从之前的上下文中寻找.
var a = 1;
function b () {
console.log(a);
a = 10;
return;
function a(){ }
}
b();
console.log(a);
//function a() {}, 1
作用域链
- VO中包含一个额外的属性,该属性指向创建该VO的函数本身(全局上下文除外),如果是全局中的函数,则这个函数的额外属性指向全局上下文
- 每个函数在创建时,会有一个隐藏属性
[[scope]]
,这个属性无法访问,它指向创建该函数时的AO(正在 执行的上下文) - 当访问一个变量时,会先查找自身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
事件循环
异步:某些函数不会立即执行,需要等到某个时机成熟后才会执行,该函数叫做异步函数
异步可以很大的提高系统吞吐量;
浏览器的线程:多线程的
- js执行引擎:负责执行JS代码—js是单线程的
- 渲染线程:负责渲染页面
- 计时器线程:负责计时—setTimeout函数的计时是靠这个方法的;
- 事件监听线程:负责监听事件
- 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)
}
*/
}